Packages
ex_gram
0.67.0
0.67.0
0.66.0
0.65.0
0.64.0
0.63.0
0.62.0
0.61.0
0.60.0
0.58.0
0.57.0
0.56.1
0.56.0
0.55.1
0.55.0
0.54.0
0.53.0
0.52.2
0.52.1
0.52.0
0.51.1
0.51.0
0.50.2
0.50.1
0.50.0
0.41.0
0.40.0
0.34.0
0.33.0
0.32.0
0.31.0
0.30.0
0.29.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.1
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.2
0.6.1
0.6.0
0.5.0
0.5.0-rc6
0.5.0-rc5
0.5.0-rc4
0.5.0-rc3
0.5.0-rc2
Telegram Bot API low level and framework
Current section
Files
Jump to
Current section
Files
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