lib/ex_gram/updates/webhook.ex

defmodule ExGram.Updates.Webhook do
  @moduledoc """
  Updates implementation that uses webhook method
  """

  use GenServer
  require Logger

  @posible_updates Map.keys(%ExGram.Model.Update{})
                   |> List.delete(:__struct__)
                   |> List.delete(:update_id)

  def update(update, token_hash) do
    token_hash
    |> process_name()
    |> GenServer.cast({:update, update})
  end

  def start_link({:bot, pid, :token, token}) do
    name =
      token_hash(token)
      |> process_name()

    GenServer.start_link(__MODULE__, {:ok, pid, token}, name: name)
  end

  def init({:ok, pid, token}) do
    set_webhook(token)

    {:ok, {pid, token}}
  end

  def handle_cast({:update, update}, {pid, token}) do
    GenServer.call(pid, {:update, update})

    {:noreply, {pid, token}}
  end

  def handle_info(unknown_message, state) do
    Logger.debug("Webhook updates received an unknown message #{inspect(unknown_message)}")

    {:noreply, state}
  end

  defp process_name(token_hash), do: Module.concat(__MODULE__, token_hash)

  defp token_hash(token) do
    :crypto.hash(:sha, token)
    |> Base.url_encode64(padding: true)
  end

  defp set_webhook(token) do
    config = ExGram.Config.get(:ex_gram, :webhook)
    params = webhook_params(config)

    case config[:url] do
      webhook_url when is_binary(webhook_url) ->
        case ExGram.set_webhook(
               "https://#{webhook_url}/telegram/#{token_hash(token)}",
               [{:token, token} | params]
             ) do
          {:ok, _} -> nil
          {:error, error} -> Logger.error("Could not set the webhook: #{inspect(error)}")
        end

      nil ->
        Logger.warning(
          "The webhook_url is not set in the configuration. Please manually set the webhook using this method: https://core.telegram.org/bots/api#setwebhook"
        )
    end
  end

  defp webhook_params(_, params \\ [])
  defp webhook_params([], params), do: params

  defp webhook_params([{:certificate, path} | tl], params) do
    case File.read(path) do
      {:ok, _} ->
        webhook_params(tl, [{:certificate, {:file, path}} | params])

      {:error, reason} ->
        Logger.error("Could not read the certificate file from #{path}: #{inspect(reason)}")

        webhook_params(tl, params)
    end
  end

  defp webhook_params([{:url, _} | tl], params), do: webhook_params(tl, params)

  defp webhook_params([{:max_connections, max_connections} | tl], params)
       when is_integer(max_connections) do
    webhook_params(tl, [{:max_connections, max_connections} | params])
  end

  defp webhook_params([{:max_connections, max_connections} | tl], params) do
    webhook_params(tl, [{:max_connections, String.to_integer(max_connections)} | params])
  end

  defp webhook_params([{:allowed_updates, allowed_updates} | tl], params)
       when is_list(allowed_updates) do
    allowed_updates =
      Enum.map(allowed_updates, fn update ->
        if String.to_atom(update) in @posible_updates do
          update
        else
          Logger.error("The update #{update} is not a valid update")

          nil
        end
      end)
      |> Enum.reject(&is_nil/1)

    webhook_params(tl, [{:allowed_updates, allowed_updates} | params])
  end

  @secret_token_length 1..256
  @secret_token_format ~r/^[A-Za-z0-9_-]+$/
  defp webhook_params([{:secret_token, secret_token} | tl], params) do
    with true <- String.length(secret_token) in @secret_token_length,
         true <- String.match?(secret_token, @secret_token_format) do
      webhook_params(tl, [{:secret_token, secret_token} | params])
    else
      _ ->
        Logger.error(
          "The secret_token must be between 1 to 256 characters. Only characters A-Z, a-z, 0-9, _ and - are allowed."
        )

        webhook_params(tl, params)
    end
  end

  defp webhook_params([{key, value} | tl], params),
    do: webhook_params(tl, [{key, value} | params])
end