Packages
ex_gram
0.55.1
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/dispatcher.ex
defmodule ExGram.Dispatcher do
@moduledoc """
Named process that receive the updates, apply the middlewares for the bot and call the bot's handler
"""
use GenServer
alias ExGram.Bot
alias ExGram.Cnt
alias ExGram.Model
@type custom_key() :: any()
@type parsed_message() ::
{:command, key :: String.t() | custom_key(), Model.Message.t()}
| {:text, String.t(), Model.Message.t()}
| {:regex, key :: custom_key(), Model.Message.t()}
| {:location, Model.Location.t()}
| {:message, Model.Message.t()}
| {:callback_query, Model.CallbackQuery.t()}
| {:inline_query, Model.InlineQuery.t()}
| {:edited_message, Model.Message.t()}
| {:update, Model.Update.t()}
@type t() :: %__MODULE__{
name: atom(),
bot_info: Model.User.t() | nil,
dispatcher_name: atom(),
extra_info: map(),
commands: %{String.t() => map()},
regex: [Regex.t()],
middlewares: [Bot.middleware()],
handler: {module(), atom()},
error_handler: {module(), atom()}
}
defstruct name: __MODULE__,
bot_info: nil,
dispatcher_name: __MODULE__,
extra_info: %{},
commands: %{},
regex: [],
middlewares: [],
handler: nil,
error_handler: nil
@spec new(Enumerable.t()) :: t()
def new(overrides \\ %{}) do
struct!(__MODULE__, overrides)
end
@spec init_state(atom(), Model.User.t() | nil, module()) :: t()
@spec init_state(atom(), Model.User.t() | nil, module(), map()) :: t()
def init_state(name, bot_info, module, extra_info \\ %{}) when is_atom(name) and is_atom(module) do
%__MODULE__{
name: name,
bot_info: bot_info,
dispatcher_name: name,
extra_info: extra_info,
commands: prepare_commands(module.commands()),
regex: module.regexes(),
middlewares: module.middlewares(),
handler: {module, :handle},
error_handler: {module, :handle_error}
}
end
@spec prepare_commands([Keyword.t()]) :: %{String.t() => map()}
defp prepare_commands(commands) when is_list(commands) do
Map.new(commands, fn command ->
command = Map.new(command)
{command.command, command}
end)
end
@spec start_link(t()) :: GenServer.on_start()
def start_link(%__MODULE__{dispatcher_name: name} = state) do
GenServer.start_link(__MODULE__, state, name: name)
end
@impl GenServer
def init(%__MODULE__{} = state) do
{:ok, state}
end
@impl GenServer
def handle_call({:update, update}, _from, %__MODULE__{} = state) do
cnt = %Cnt{default_context(state) | update: update}
cnt = apply_middlewares(cnt)
unless cnt.halted do
info = extract_info(cnt)
spawn(fn -> call_handler(info, cnt, state) end)
end
{:reply, :ok, state}
end
def handle_call({:message, origin, msg}, from, %__MODULE__{} = state) do
bot_message = {:bot_message, origin, msg}
cnt = build_context_with_middlewares(state, bot_message, %{from: from})
if cnt.halted do
{:reply, :halted, state}
else
response = call_handler(bot_message, cnt, state)
{:reply, response, state}
end
end
def handle_call(msg, from, %__MODULE__{} = state) do
message = {:call, msg}
cnt = build_context_with_middlewares(state, message, %{from: from})
if cnt.halted do
{:reply, :halted, state}
else
response = call_handler(message, cnt, state)
{:reply, response, state}
end
end
# EditedMessage
# ChannelPost
# EditedChannelPost
# InlineQuery
# ChosenInlineResult
@impl GenServer
def handle_cast(msg, %__MODULE__{} = state) do
message = {:cast, msg}
cnt = build_context_with_middlewares(state, message)
unless cnt.halted do
spawn(fn -> call_handler(message, cnt, state) end)
end
{:noreply, state}
end
@impl GenServer
def handle_info(msg, %__MODULE__{} = state) do
message = {:info, msg}
cnt = %Cnt{default_context(state) | message: message}
cnt = apply_middlewares(cnt)
unless cnt.halted do
call_handler(message, cnt, state)
end
{:noreply, state}
end
@spec default_context(t()) :: Cnt.t()
defp default_context(%__MODULE__{
name: name,
bot_info: bot_info,
extra_info: extra_info,
middlewares: middlewares,
commands: commands,
regex: regex
}) do
%Cnt{
name: name,
bot_info: bot_info,
halted: false,
middlewares: middlewares,
commands: commands,
regex: regex,
extra: extra_info
}
end
defp build_context_with_middlewares(state, message, extra \\ %{}) do
%Cnt{default_context(state) | message: message}
|> ExGram.Middleware.add_extra(extra)
|> apply_middlewares()
end
@spec handle_text(String.t(), Cnt.t()) ::
{:command, key :: String.t() | custom_key(), text :: String.t()}
| {:text, String.t()}
| {:regex, key :: custom_key(), text :: String.t()}
defp handle_text("/" <> text, %Cnt{commands: commands}) do
{cmd, text} =
case String.split(text, " ", parts: 2) do
[cmd] -> {cmd, ""}
[cmd, rest] -> {cmd, rest}
end
case Map.get(commands, cmd) do
%{name: name} -> {:command, name, text}
nil -> {:command, cmd, text}
end
end
defp handle_text(text, %Cnt{regex: []}) do
{:text, text}
end
defp handle_text(text, %Cnt{regex: regex}) do
case Enum.find(regex, &Regex.match?(Keyword.get(&1, :regex), text)) do
nil -> {:text, text}
reg -> {:regex, Keyword.get(reg, :name), text}
end
end
@spec extract_info(Cnt.t()) :: parsed_message()
defp extract_info(%Cnt{update: %{message: %{text: text} = message}} = cnt) when is_binary(text) do
case handle_text(text, cnt) do
{:command, key, text} -> {:command, key, %{message | text: text}}
{:text, text} -> {:text, text, %{message | text: text}}
{:regex, key, text} -> {:regex, key, %{message | text: text}}
end
end
defp extract_info(%Cnt{update: %{message: %{location: %{} = location}}}) do
{:location, location}
end
defp extract_info(%Cnt{update: %{message: %{} = message}}) do
{:message, message}
end
defp extract_info(%Cnt{update: %{callback_query: %{} = callback_query}}) do
{:callback_query, callback_query}
end
defp extract_info(%Cnt{update: %{inline_query: %{} = inline_query}}) do
{:inline_query, inline_query}
end
defp extract_info(%Cnt{update: %{edited_message: %{} = edited_message}}) do
{:edited_message, edited_message}
end
# channel_post
# edited_channel_post
# chosen_inline_result
# shipping_query
# pre_checkout_query
defp extract_info(%Cnt{update: update}) do
{:update, update}
end
defp apply_middlewares(%Cnt{middlewares: []} = cnt), do: cnt
defp apply_middlewares(%Cnt{halted: true} = cnt), do: cnt
defp apply_middlewares(%Cnt{middleware_halted: true} = cnt), do: cnt
defp apply_middlewares(%Cnt{middlewares: [{fun, opts} | rest]} = cnt) when is_function(fun, 2) do
%Cnt{cnt | middlewares: rest}
|> fun.(opts)
|> apply_middlewares()
end
defp apply_middlewares(%Cnt{middlewares: [{module, opts} | rest]} = cnt) when is_atom(module) do
init_opts = module.init(opts)
%Cnt{cnt | middlewares: rest}
|> module.call(init_opts)
|> apply_middlewares()
end
defp apply_middlewares(%Cnt{middlewares: [_ | rest]} = cnt) do
apply_middlewares(%Cnt{cnt | middlewares: rest})
end
defp call_handler(info, cnt, %__MODULE__{handler: {module, method}} = state) do
case apply(module, method, [info, cnt]) do
%Cnt{} = cnt ->
%Cnt{responses: responses} = ExGram.Dsl.send_answers(cnt)
handle_responses(responses, state)
_ ->
:noop
end
end
defp handle_responses([], _state), do: []
defp handle_responses([{:error, error} | rest], %{error_handler: {module, method}} = state) do
apply(module, method, [error])
handle_responses(rest, state)
end
defp handle_responses([value | rest], state) do
[value | handle_responses(rest, state)]
end
end