Current section

Files

Jump to
ex_gram lib ex_gram plug.ex
Raw

lib/ex_gram/plug.ex

if Code.ensure_loaded?(Plug) do
defmodule ExGram.Plug do
@moduledoc """
A Plug to handle incoming Telegram updates in webhook mode. It checks for the secret
token (if configured) and processes the update accordingly.
This plug is used with `ExGram.Updates.Webhook` to receive updates from Telegram.
See the [Polling and Webhooks](polling-and-webhooks.md) guide for setup instructions.
"""
@behaviour Plug
import Plug.Conn
alias ExGram.Model.Update
alias Plug.Conn
require Logger
@impl true
def init(opts) do
path = opts[:path] || ExGram.Config.get(:ex_gram, :webhook)[:path] || "/telegram"
path_parts = String.split(path, "/", trim: true)
[path_parts: path_parts]
end
@impl true
def call(%Conn{method: "POST"} = conn, opts) do
if path_match?(conn, opts) do
handle_update(conn)
else
conn
end
end
def call(conn, _), do: conn
defp path_match?(%Conn{path_info: path_info}, opts) do
path_parts = Keyword.fetch!(opts, :path_parts)
path_parts_length = length(path_parts)
# path can be /a/b/c, we should check if all the first parts match,
# and there's only one extra part (the token hash)
Enum.take(path_info, path_parts_length) == path_parts and
length(path_info) == path_parts_length + 1
end
defp handle_update(conn) do
{:ok, body, conn} = Conn.read_body(conn)
{status, message} =
case conn
|> get_req_header("x-telegram-bot-api-secret-token")
|> check_secret_token() do
:ok -> handle_update(conn, ExGram.Encoder.decode(body, keys: :atoms))
{:error, error} -> {400, %{error: error}}
end
conn
|> put_resp_content_type("application/json")
|> send_resp(status, ExGram.Encoder.encode!(message))
|> halt()
end
defp handle_update(conn, {:ok, update}) do
token_hash = token_hash(conn.path_info)
case ExGram.Cast.cast(update, Update) do
{:ok, update} ->
ExGram.Updates.Webhook.update(update, token_hash)
{200, %{ok: true}}
{:error, error} ->
Logger.error("Failed to cast update: #{Exception.message(error)}")
{400, %{ok: false, error: "Body is not a valid Telegram update"}}
end
end
defp handle_update(_conn, {:error, error}), do: {400, %{error: Exception.message(error)}}
defp check_secret_token([]) do
Logger.debug(
"The secret token is not configured in webhook mode. For security purposes, it is recommended to set one."
)
:ok
end
defp check_secret_token([secret_token]) do
config_token = ExGram.Config.get(:ex_gram, :webhook)[:secret_token]
case {secret_token, config_token} do
{_secret_token, nil} ->
message =
"The secret token exists in the request header but is not configured in the application's settings."
Logger.error(message)
{:error, message}
{secret_token, config_token} when secret_token != config_token ->
message =
"The secret token in the request header does not match the one configured in the webhook mode."
Logger.error(message)
{:error, message}
{secret_token, config_token} when secret_token == config_token ->
:ok
end
end
defp token_hash([path]) when is_binary(path), do: path
defp token_hash([_ | tl]), do: token_hash(tl)
end
end