Current section

Files

Jump to
ex_gram lib ex_gram updates webhook.ex
Raw

lib/ex_gram/updates/webhook.ex

defmodule ExGram.Updates.Webhook do
@moduledoc """
Updates implementation using the webhook method.
This GenServer receives updates pushed from Telegram via webhooks. On startup,
it automatically calls `ExGram.set_webhook/2` to register the webhook URL with
Telegram.
The webhook URL is constructed as `<base_url>/<path>/<token_hash>`, where
`token_hash` is a Base64-encoded SHA hash of the bot token.
The default <path> is `/telegram`, but it can be customized via the `:path` option in the configuration.
Configure the webhook with `config :ex_gram, updates: ExGram.Updates.Webhook, webhook: [...]`.
See the [Polling and Webhooks guide](polling-and-webhooks.md) and `ExGram.Plug`
for more details.
"""
use GenServer
require Logger
@posible_updates %ExGram.Model.Update{}
|> Map.keys()
|> 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: bot, token: token} = opts) do
opts = Map.drop(opts, [:bot, :token])
name =
token
|> token_hash()
|> process_name()
GenServer.start_link(__MODULE__, {:ok, bot, token, opts}, name: name)
end
def init({:ok, bot, token, opts}) do
Process.flag(:trap_exit, true)
start_time = ExGram.Telemetry.start([:updates, :init], %{bot: bot, method: :webhook})
set_webhook(token, opts)
state = %{bot: bot}
ExGram.Telemetry.stop([:updates, :init], start_time, %{bot: bot, method: :webhook})
{:ok, state}
end
def terminate(_reason, %{bot: bot}) do
ExGram.Telemetry.emit([:updates, :shutdown], %{bot: bot, method: :webhook})
end
def handle_cast({:update, update}, %{bot: bot} = state) do
GenServer.call(bot, {:update, update})
{:noreply, state}
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
:sha
|> :crypto.hash(token)
|> Base.url_encode64(padding: true)
end
defp set_webhook(token, opts) do
opts =
opts
|> Map.take([
:certificate,
:url,
:max_connections,
:allowed_updates,
:secret_token,
:drop_pending_updates,
:ip_address,
:path
])
|> Keyword.new()
config = :ex_gram |> ExGram.Config.get(:webhook, []) |> Keyword.merge(opts)
params = webhook_params(config)
case valid_url(config[:url]) do
{:ok, webhook_uri} ->
params = Keyword.put(params, :token, token)
base_path = config[:path] || "/telegram"
path = Path.join([webhook_uri.path || "/", base_path, token_hash(token)])
url = URI.to_string(%{webhook_uri | path: path})
{:ok, true} = ExGram.set_webhook(url, params)
{:error, error} ->
Logger.error(
"The webhook_url is wrong with reason: #{inspect(error)}. Please manually set the webhook using this method: https://core.telegram.org/bots/api#setwebhook or edit the config file!"
)
end
end
defp valid_url(nil), do: {:error, :not_set}
defp valid_url(url), do: url |> URI.parse() |> do_valid_url()
defp do_valid_url(%URI{scheme: nil}), do: {:error, :scheme_not_set}
defp do_valid_url(%URI{scheme: scheme}) when scheme not in ["http", "https"], do: {:error, :scheme_is_wrong}
defp do_valid_url(%URI{host: nil}), do: {:error, :host_not_set}
defp do_valid_url(%URI{} = uri), do: {:ok, uri}
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([{:path, _} | 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 =
allowed_updates
|> Enum.map(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