Current section
Files
Jump to
Current section
Files
lib/server.ex
defmodule TelegramEx.Server do
@moduledoc """
GenServer that long-polls Telegram for updates and dispatches them to handlers.
This module implements the core polling loop that fetches updates from Telegram
and routes them to the appropriate handlers in your bot module and routers.
## Responsibilities
- **Long polling**: Continuously fetches updates from Telegram API
- **Message parsing**: Converts raw Telegram updates into typed structs
- **Handler dispatch**: Routes messages and callbacks to appropriate handlers
- **FSM integration**: Loads and saves per-user state before/after handlers
- **Router chain**: Tries routers in order before falling back to main bot module
## Handler Chain
When an update arrives, handlers are tried in this order:
1. Each router module (in the order specified)
2. The main bot module
If a handler returns `:pass`, the next handler in the chain is tried.
Otherwise, the result is processed and the chain stops.
"""
use GenServer
require Logger
alias TelegramEx.{API, Config, FSM, Types}
@type chat_id :: TelegramEx.Types.chat_id()
@typedoc """
Internal server state.
Tracks bot configuration, routers, and current update offset for polling.
"""
@type state :: %{
bot_module: module(),
bot_name: atom(),
token: String.t(),
routers: list(module()),
offset: integer()
}
@spec start_link(module(), atom(), list(module())) :: GenServer.on_start()
def start_link(bot_module, bot_name, routers \\ []),
do: GenServer.start_link(__MODULE__, {bot_module, bot_name, routers}, name: bot_name)
@impl true
def init({bot_module, bot_name, routers}) do
case FSM.init(bot_name) do
:ok ->
state = %{
bot_module: bot_module,
bot_name: bot_name,
routers: routers,
token: Config.token(bot_name),
offset: 0
}
{:ok, state, {:continue, :start_polling}}
{:error, reason} ->
Logger.error("FSM initialization failed: #{reason}")
{:stop, reason}
end
end
@impl true
def handle_continue(:start_polling, state) do
Task.start_link(fn -> poll_updates(state) end)
{:noreply, state}
end
@spec poll_updates(state()) :: no_return()
defp poll_updates(%{token: token, offset: offset} = state) do
Process.put(:token, token)
case API.get_updates(token, offset) do
{:ok, updates} ->
Enum.each(updates, &process_update(&1, state))
new_offset =
case updates do
[] -> offset
updates -> List.last(updates)["update_id"] + 1
end
poll_updates(%{state | offset: new_offset})
{:error, reason} ->
Logger.error("Error while getting update: #{inspect(reason)}")
poll_updates(state)
end
end
@spec process_update(map(), state()) :: :ok | {:error, term()}
defp process_update(update, %{
bot_module: bot_module,
bot_name: bot_name,
token: token,
routers: routers
}) do
cond do
update["message"] ->
update["message"]
|> parse_message()
|> run_handler(bot_module, bot_name, token, routers, :handle_message)
update["callback_query"] ->
update["callback_query"]
|> parse_callback_query()
|> run_handler(bot_module, bot_name, token, routers, :handle_callback)
true ->
:ok
end
end
@spec run_handler(
Types.Message.t() | Types.CallbackQuery.t(),
module(),
atom(),
String.t(),
list(module()),
atom()
) :: :ok | {:error, term()}
defp run_handler(message, bot_module, bot_name, token, routers, handler) do
chat_id = get_chat_id(message)
{state, data} = FSM.get_state(bot_name, chat_id)
ctx = %{state: state, data: data, token: token}
ctx =
if message.message_thread_id do
Map.put(ctx, :message_thread_id, message.message_thread_id)
else
ctx
end
modules = routers ++ [bot_module]
Enum.reduce_while(modules, :pass, fn module, :pass ->
case apply(module, handler, [message, ctx]) do
:pass -> {:cont, :pass}
result -> {:halt, result}
end
end)
|> handle_result(bot_name, chat_id, state)
end
defp handle_result({:transition, new_state, data}, bot_name, chat_id, _state),
do: FSM.set_state(bot_name, chat_id, new_state, data)
defp handle_result({:transition, new_state}, bot_name, chat_id, _state),
do: FSM.set_state(bot_name, chat_id, new_state)
defp handle_result({:stay, data}, bot_name, chat_id, state),
do: FSM.set_state(bot_name, chat_id, state, data)
defp handle_result(:ok, _bot_name, _chat_id, _state), do: :ok
defp handle_result(:pass, _bot_name, _chat_id, _state), do: :ok
defp handle_result({:error, reason}, _bot_name, _chat_id, _state),
do: Logger.error("Handler error: #{inspect(reason)}")
defp handle_result(error, _bot_name, _chat_id, _state),
do: Logger.error("Unknown handler response: #{inspect(error)}")
@spec get_chat_id(Types.Message.t() | Types.CallbackQuery.t()) :: chat_id()
defp get_chat_id(%Types.CallbackQuery{message: %{chat: chat}}), do: chat["id"]
defp get_chat_id(%Types.Message{chat: chat}), do: chat["id"]
@spec parse_message(map()) :: Types.Message.t()
defp parse_message(message), do: Types.Message.from_map(message)
@spec parse_callback_query(map()) :: Types.CallbackQuery.t()
defp parse_callback_query(callback_query), do: Types.CallbackQuery.from_map(callback_query)
end