<p align="center">
<img src="https://img.shields.io/badge/elixir-%3E%3D%201.14-blueviolet?style=flat-square" />
<img src="https://img.shields.io/badge/telegram-bot%20api%20%2B%20tdlib-26A5E4?style=flat-square&logo=telegram&logoColor=white" />
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" />
</p>
<h1 align="center">Sexy</h1>
<p align="center">
<b>Telegram framework for Elixir — bots and userbots from one dependency</b><br/>
<sub>Sexy.Bot for Bot API. Sexy.TDL for TDLib. Use one or both.</sub>
</p>
---
## What is Sexy?
Sexy is a Telegram framework with two engines:
- **Sexy.Bot** — Bot API with a single-message UI pattern. Every screen replaces the previous one, creating an app-like experience inside Telegram.
- **Sexy.TDL** — TDLib integration for userbot sessions. Manages port to `tdlib_json_cli`, deserializes JSON into Elixir structs, routes events to your app.
Both can run in the same application simultaneously.
---
## Quick Start: Bot API
### 1. Add dependency
```elixir
# mix.exs
defp deps do
[{:sexy, git: "git@github.com:Puremag1c/Sexy.git"}]
end
```
### 2. Start in your supervision tree
```elixir
children = [
{Sexy.Bot, token: "123456:ABC-DEF...", session: MyApp.Session},
]
```
### 3. Implement Session
```elixir
defmodule MyApp.Session do
@behaviour Sexy.Bot.Session
# Persistence — Sexy manages one active message per chat
@impl true
def get_message_id(chat_id), do: MyApp.Users.get_mid(chat_id)
@impl true
def on_message_sent(chat_id, message_id, type, extra) do
MyApp.Users.save_mid(chat_id, message_id, type, extra)
end
# Dispatch — Sexy routes updates to these callbacks
@impl true
def handle_command(update), do: MyApp.Bot.command(update)
@impl true
def handle_query(update), do: MyApp.Bot.query(update)
@impl true
def handle_message(update), do: MyApp.Bot.message(update)
@impl true
def handle_chat_member(update), do: :ok
end
```
### 4. Build and send screens
```elixir
%{
chat_id: chat_id,
text: "Welcome!",
kb: %{inline_keyboard: [[%{text: "Start", callback_data: "/start"}]]}
}
|> Sexy.Bot.build()
|> Sexy.Bot.send()
```
That's it. Sexy deletes the old message, sends the new one, and saves state via your Session.
---
## Quick Start: TDLib (Userbots)
### 1. Configure
```elixir
# config/config.exs
config :sexy,
tdlib_binary: "/path/to/tdlib_json_cli",
tdlib_data_root: "/path/to/tdlib_data"
```
Or run the interactive setup: `mix sexy.tdl.setup`
### 2. Add to supervision tree
```elixir
children = [
Sexy.TDL,
# optionally alongside Sexy.Bot:
{Sexy.Bot, token: "...", session: MyApp.Session},
]
```
### 3. Open a session
```elixir
config = %{Sexy.TDL.default_config() |
api_id: "12345",
api_hash: "abc123",
database_directory: "/tmp/tdlib_data/my_account"
}
Sexy.TDL.open("my_account", config, app_pid: self())
```
### 4. Handle events
```elixir
def handle_info({:recv, struct}, state) do
# TDLib object as Elixir struct (e.g. %Sexy.TDL.Object.UpdateNewMessage{})
end
def handle_info({:proxy_event, text}, state) do
# proxychains output
end
def handle_info({:system_event, type, details}, state) do
# :port_failed, :port_exited, :proxy_conf_missing
end
```
### 5. Send commands
```elixir
Sexy.TDL.transmit("my_account", %Sexy.TDL.Method.GetMe{})
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 userbot!"}
}
})
```
---
## Concepts
### Single-message pattern (Bot)
```
User clicks button -> old message deleted -> new message sent -> state saved
```
Every chat has one active screen. `Sexy.Bot.send/1` handles the full cycle: detect content type, call Telegram API, delete previous message via `Session.get_message_id/1`, save new mid via `Session.on_message_sent/4`.
### Object struct
Every message goes through `Sexy.Utils.Object` — the universal message container. Build one with `Sexy.Bot.build/1`:
```elixir
Sexy.Bot.build(%{chat_id: 123, text: "Hello!"})
#=> %Sexy.Utils.Object{chat_id: 123, text: "Hello!", ...}
```
**Fields:**
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `chat_id` | integer | `nil` | Telegram chat id (**required**) |
| `text` | string | `""` | Message text or caption (HTML supported) |
| `media` | string/nil | `nil` | Content type selector (see table below) |
| `kb` | map | `%{inline_keyboard: []}` | Telegram reply markup |
| `entity` | list | `[]` | Telegram entities (bold, links, etc.). When non-empty, `parse_mode` is omitted |
| `update_data` | map | `%{}` | App-specific data passed to `Session.on_message_sent/4` as `extra` |
| `file` | binary/nil | `nil` | File content for document uploads |
| `filename` | string/nil | `nil` | Filename for document uploads |
**Media type detection** — the `media` field determines how the message is sent:
| `media` value | Sent as | API method |
|---------------|---------|------------|
| `nil` | text message | `sendMessage` |
| `"file"` | document (multipart upload) | `sendDocument` |
| starts with `"A"` | photo | `sendPhoto` |
| starts with `"B"` | video | `sendVideo` |
| starts with `"C"` | animation (GIF) | `sendAnimation` |
Telegram file_ids have predictable prefixes by type — Sexy uses this for auto-detection.
**Examples:**
```elixir
# Text message with buttons
%{chat_id: id, text: "Pick:", kb: %{inline_keyboard: [[%{text: "Go", callback_data: "/go"}]]}}
# Photo by file_id
%{chat_id: id, text: "Nice photo", media: "AgACAgIAAxk..."}
# Document upload from binary
%{chat_id: id, text: "Your report", media: "file", file: csv_binary, filename: "report.csv"}
# Pass state to on_message_sent
%{chat_id: id, text: "Cart", update_data: %{screen: "cart", page: 1}}
```
### `send/2` options
`Sexy.Bot.send(object, opts)` sends an Object and manages the message lifecycle.
| Option | Default | Description |
|--------|---------|-------------|
| `update_mid: true` | `true` | Delete previous message, save new mid via Session |
| `update_mid: false` | — | Send without touching the current screen state |
```elixir
# Normal send — replaces current screen (default)
Sexy.Bot.build(%{chat_id: id, text: "Home"}) |> Sexy.Bot.send()
# Send without replacing — useful for secondary messages
Sexy.Bot.build(%{chat_id: id, text: "Tip of the day"}) |> Sexy.Bot.send(update_mid: false)
```
### Notifications
`Sexy.Bot.notify(chat_id, message, opts)` sends notification messages separate from the main screen flow.
**Options:**
| Option | Default | Description |
|--------|---------|-------------|
| `replace: false` | `false` | **Overlay** — sends without replacing current screen, adds dismiss button |
| `replace: true` | — | **Replace** — becomes new active screen (mid updated via Session) |
| `navigate: {text, path}` | `nil` | Adds a button that deletes the notification and calls `Session.handle_transit/3` |
| `navigate: {text, fn}` | `nil` | Same, but with a function `fn mid -> callback_data end` for custom routing |
| `dismiss_text: "text"` | `"OK"` | Custom dismiss button text |
| `extra_buttons: [[...]]` | `[]` | Additional button rows appended after navigate/dismiss |
```elixir
# Overlay — dismiss button, current screen untouched
Sexy.Bot.notify(chat_id, %{text: "Done!"})
# Custom dismiss text
Sexy.Bot.notify(chat_id, %{text: "Saved!"}, dismiss_text: "Got it")
# Replace — becomes new active screen
Sexy.Bot.notify(chat_id, %{text: "Payment received!"}, replace: true)
# Navigate — click deletes notification, calls Session.handle_transit/3
Sexy.Bot.notify(chat_id, %{text: "New order!"},
navigate: {"View Order", "/order id=123"}
)
# Navigate with custom callback + extra buttons
Sexy.Bot.notify(chat_id, %{text: "Alert!"},
navigate: {"Details", fn mid -> "/show mid=#{mid}" end},
extra_buttons: [[%{text: "Mute", callback_data: "/mute"}]]
)
```
### TDL supervision tree
```
Sexy.TDL (Supervisor)
|-- Sexy.TDL.Registry (ETS session storage)
|-- AccountVisor (DynamicSupervisor)
|-- Riser per session (one_for_all)
|-- Backend (port to tdlib_json_cli)
|-- Handler (JSON -> structs -> events)
|-- ...extra children from your app
```
Open a session with `Sexy.TDL.open/3`, close with `Sexy.TDL.close/1`. Each session gets its own supervision subtree. Pass `children: [MyWorker]` in opts to inject app-specific processes.
### Auto-generated types
Sexy ships 2558 structs generated from TDLib API documentation:
- `Sexy.TDL.Method.*` — 786 API methods (GetMe, SendMessage, etc.)
- `Sexy.TDL.Object.*` — 1772 response types (UpdateNewMessage, User, Chat, etc.)
Regenerate from a different TDLib version: `mix sexy.tdl.generate_types /path/to/types.json`
---
## API Reference
### Sexy.Bot
| Function | Description |
|----------|-------------|
| `build(map)` | Map -> Object struct |
| `send(object, opts)` | Send to Telegram, manage mid lifecycle |
| `notify(chat_id, msg, opts)` | Notification with dismiss/navigate |
| `send_message(chat_id, text)` | Send text message |
| `send_photo(body)` | Send photo |
| `send_video(body)` | Send video |
| `send_animation(body)` | Send animation |
| `send_document(chat_id, file, name, text, kb)` | Send file |
| `edit_text(body)` | Edit message text |
| `edit_reply_markup(body)` | Edit buttons |
| `delete_message(chat_id, mid)` | Delete message |
| `answer_callback(id, text, alert)` | Answer callback query |
| `send_invoice(chat_id, title, desc, payload, cur, prices)` | Telegram Stars payment |
| `request(body, method)` | Any Telegram Bot API method |
### Sexy.TDL
| Function | Description |
|----------|-------------|
| `open(session, config, opts)` | Start TDLib session |
| `close(session)` | Stop session and cleanup |
| `transmit(session, msg)` | Send command to TDLib |
| `default_config()` | Base TDLib config template |
### Sexy.Bot.Session callbacks
| Callback | Required | Description |
|----------|----------|-------------|
| `get_message_id(chat_id)` | yes | Return current active mid |
| `on_message_sent(chat_id, mid, type, extra)` | yes | Save new active mid |
| `handle_command(update)` | yes | `/command` messages |
| `handle_query(update)` | yes | Button callbacks |
| `handle_message(update)` | yes | Text messages |
| `handle_chat_member(update)` | yes | Join/leave events |
| `handle_poll(update)` | no | Poll responses |
| `handle_transit(chat_id, cmd, query)` | no | Transit button clicks |
---
## Module Map
```
Sexy Namespace module
Sexy.Bot Bot API supervisor + public API
Sexy.Bot.Api Telegram HTTP client
Sexy.Bot.Sender Object -> Telegram + mid lifecycle
Sexy.Bot.Screen Map -> Object struct
Sexy.Bot.Session Behaviour: persistence + dispatch
Sexy.Bot.Notification Overlay/replace notifications
Sexy.Bot.Poller GenServer polling + routing
Sexy.TDL TDLib supervisor + open/close/transmit API
Sexy.TDL.Backend Port to tdlib_json_cli binary
Sexy.TDL.Handler JSON deserialization + event routing
Sexy.TDL.Registry ETS session storage
Sexy.TDL.Riser Per-account supervisor
Sexy.TDL.Object 1772 auto-generated TDLib object structs
Sexy.TDL.Method 786 auto-generated TDLib method structs
Sexy.Utils Query parsing, formatting, type conversion
Sexy.Utils.Bot Command parsing, pagination
Sexy.Utils.Object Message struct + type detection
```
---
## Mix Tasks
| Task | Description |
|------|-------------|
| `mix sexy.tdl.setup` | Interactive TDLib configuration wizard |
| `mix sexy.tdl.generate_types [path]` | Regenerate Method/Object structs from types.json |
---
## Migration
Upgrading from an older version? See [MIGRATION.md](MIGRATION.md).
## License
MIT