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