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 the bot module.
Started automatically via `child_spec/1` injected by `use TelegramEx`.
"""
use GenServer
require Logger
alias TelegramEx.{API, Config, FSM, Types}
@type chat_id :: TelegramEx.Types.chat_id()
@type state :: %{
bot_module: module(),
bot_name: atom(),
token: String.t(),
offset: integer()
}
@spec start_link(module(), atom()) :: GenServer.on_start()
def start_link(bot_module, bot_name),
do: GenServer.start_link(__MODULE__, {bot_module, bot_name}, name: bot_name)
@impl true
def init({bot_module, bot_name}) do
case FSM.init(bot_name) do
:ok ->
state = %{
bot_module: bot_module,
bot_name: bot_name,
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: #{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}) do
cond do
update["message"] ->
update["message"]
|> parse_message()
|> run_handler(bot_module, bot_name, :handle_message)
update["callback_query"] ->
update["callback_query"]
|> parse_callback_query()
|> run_handler(bot_module, bot_name, :handle_callback)
true ->
:ok
end
end
@spec run_handler(
Types.Message.t() | Types.CallbackQuery.t(),
module(),
atom(),
atom()
) :: :ok | {:error, term()}
defp run_handler(message, bot_module, bot_name, handler) do
chat_id = get_chat_id(message)
{state, data} = FSM.get_state(bot_name, chat_id)
if function_exported?(bot_module, handler, 3) and state do
apply(bot_module, handler, [message, state, data])
else
apply(bot_module, handler, [message])
end
|> case do
{:transition, new_state, data} ->
FSM.set_state(bot_name, chat_id, new_state, data)
{:transition, new_state} ->
FSM.set_state(bot_name, chat_id, new_state)
{:stay, data} ->
FSM.set_state(bot_name, chat_id, state, data)
:ok ->
:ok
{:error, reason} ->
Logger.error("Handler error: #{inspect(reason)}")
error ->
Logger.error("Unknown handler response: #{inspect(error)}")
end
end
@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