Build a Telegram userbot with Sexy.TDL.
Prerequisites
tdlib_json_clibinary installed (or built from TDLib source)- Telegram API credentials from my.telegram.org
1. Configure
# config/config.exs
config :sexy,
tdlib_binary: "/usr/local/bin/tdlib_json_cli",
tdlib_data_root: "/tmp/tdlib_data"Or use the interactive wizard:
mix sexy.tdl.setup
2. Add to supervision tree
children = [
Sexy.TDL
]3. Open a session
config = %{Sexy.TDL.default_config() |
api_id: "12345",
api_hash: "abc123def456",
database_directory: "/tmp/tdlib_data/my_account"
}
{:ok, _pid} = Sexy.TDL.open("my_account", config, app_pid: self())4. Handle events
All TDLib events arrive as messages to the app_pid process:
defmodule MyApp.TDLWorker do
use GenServer
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
def init(opts) do
config = %{Sexy.TDL.default_config() |
api_id: opts[:api_id],
api_hash: opts[:api_hash],
database_directory: "/tmp/tdlib_data/my_account"
}
{:ok, _} = Sexy.TDL.open("my_account", config, app_pid: self())
{:ok, %{}}
end
# TDLib authorization flow
def handle_info({:recv, %Sexy.TDL.Object.UpdateAuthorizationState{authorization_state: state}}, s) do
handle_auth(state, s)
end
# New incoming message
def handle_info({:recv, %Sexy.TDL.Object.UpdateNewMessage{message: msg}}, state) do
IO.puts("New message in chat #{msg.chat_id}")
{:noreply, state}
end
# All other TDLib events
def handle_info({:recv, _other}, state), do: {:noreply, state}
# System events
def handle_info({:system_event, type, details}, state) do
IO.puts("System event: #{type} — #{inspect(details)}")
{:noreply, state}
end
defp handle_auth(%Sexy.TDL.Object.AuthorizationStateWaitPhoneNumber{}, state) do
Sexy.TDL.transmit("my_account", %Sexy.TDL.Method.SetAuthenticationPhoneNumber{
phone_number: "+1234567890"
})
{:noreply, state}
end
defp handle_auth(_state, s), do: {:noreply, s}
end5. Send commands
# Get current user info
Sexy.TDL.transmit("my_account", %Sexy.TDL.Method.GetMe{})
# Send a message
Sexy.TDL.transmit("my_account", %Sexy.TDL.Method.SendMessage{
chat_id: 123456,
input_message_content: %Sexy.TDL.Object.InputMessageText{
text: %Sexy.TDL.Object.FormattedText{text: "Hello from Elixir!"}
}
})
# Or use a plain map
Sexy.TDL.transmit("my_account", %{
"@type" => "sendMessage",
"chat_id" => 123456,
"input_message_content" => %{
"@type" => "inputMessageText",
"text" => %{"@type" => "formattedText", "text" => "Hello!"}
}
})6. Close session
Sexy.TDL.close("my_account")Auto-generated types
Sexy ships 2558 structs matching the TDLib API:
Sexy.TDL.Method.*— 786 methods (GetMe, SendMessage, GetChat, ...)Sexy.TDL.Object.*— 1772 types (UpdateNewMessage, User, Chat, Message, ...)
Each struct has @moduledoc with field descriptions and a link to the official
Telegram documentation.
To regenerate from a newer TDLib version:
mix sexy.tdl.generate_types /path/to/types.json
Proxy support
Open a session with proxy:
Sexy.TDL.open("my_account", config, app_pid: self(), proxy: true)This requires a proxy.conf file at <tdlib_data_root>/my_account/proxy.conf
(proxychains4 format).
Running alongside Bot API
Both engines can coexist in the same supervision tree:
children = [
Sexy.TDL,
{Sexy.Bot, token: "BOT_TOKEN", session: MyApp.Session}
]