
ataraxia Handbook
Install with uv, log in through the web UI, run built-in commands and Python with .e. The inline bot runs over MTProto, and modules hot-load from any URL.
What is a userbot?
A regular Telegram bot has its own account. A userbot signs in as you and handles commands you send from your account. For example, .ping shows response time. You can send commands in Saved Messages, a private chat or a group, subject to the permissions you have there.
ataraxia is a modular userbot aimed at developers who want full control over their Telegram client. It runs on ataraxia-tl, a Telethon fork with a current protocol layer. The version is the codename Rise; branch and commit are resolved from git at startup. Licensed AGPLv3.
What it can do
- Built-in commands for diagnostics, help, translation, backups and Python evaluation
- Built-in modules plus hot-loadable external ones from any URL via
.dlm - An MTProto inline bot for forms and paginated lists, with a Bot API shaped facade
self.inline.bot - Guest commands:
@botusername cmdfrom chats the bot is not in - Rich messages from your account: headings, blockquotes and tables, with a plain fallback
- Bitmask permissions: OWNER, group admins, PM, EVERYONE, targeted rules
- Encrypted binary sessions, useless without
ataraxia.key - Web setup: browser-based auth with QR support, SSH tunnel for remote servers
- i18n via YAML packs with fallback chains; English and Russian ship built in
- Zero pip: uv manages every dependency,
pyproject.tomlis the single source of truth
Where data lives
A local installation keeps API credentials in config.json, the session master key in ataraxia.key, the encrypted session in ataraxia-<id>.atsession, a JSON database per account and external modules in loaded_modules/. Docker stores data under /data; the path can be changed with --data-root. Keep those files private.
Honest downsides
- The bot answers only while its host is running. It needs a machine that stays on
- External modules are not sandboxed: same process, access to the session and the key. Install code you trust
- Telegram formally discourages account automation. Use your head, do not spam
- ataraxia-tl follows git HEAD and is not version-pinned: an update can bump the library together with the core
One ruleSessions are encrypted with AES-256-GCM, but
ataraxia.keysits right next to them. Whoever gets both files gets the account. Do not share them, never commit them, and never back the key up together with the sessions.
Installation
Clone, then run uv run -m ataraxia. uv resolves dependencies from pyproject.toml and launches the bot.
Requirements
- Python 3.10 or newer
- uv, the project's only package manager: no pip, no manual venv
- git: the updater runs
git pull, so clone the repository instead of downloading an archive
Where to run it
A VPS is the straightforward choice for round-the-clock use. A home computer works while it is powered on. Docker users get a prepared image with node, ffmpeg and compilers inside.
| Host | Best for | Note |
|---|---|---|
| Linux VPS | 24/7 uptime | From a couple of dollars, needs ssh |
| Home PC | Trying things out | Works while powered on |
| Raspberry Pi | Quiet always-on host | Needs a separate device |
| Docker | Container setups | Data in the /data volume, port 8080 |
Linux and macOS
git clone https://git.break.wtf/xenside/ataraxia
cd ataraxia
uv run -m ataraxia
On the first run uv downloads ataraxia-tl and the remaining dependencies, which takes a couple of minutes.
Docker
docker build -t ataraxia .
docker run -d -p 8080:8080 -v ataraxia-data:/data --name ataraxia ataraxia
The image is based on python 3.14-slim with uv, node for .enode, git, ffmpeg, gcc and g++ already inside. Data persists in the /data volume; the web UI listens on port 8080.
CLI flags
| Flag | What it does |
|---|---|
--port | Web UI port. Random free port by default, always 8080 in Docker |
--no-web | Disable the web UI, log in from the terminal |
--data-root | Root path for session and data files |
--phone, -p | Phone number to log in; can be passed multiple times |
--no-auth | Skip authentication and API token input, exit if needed |
--proxy-host, --proxy-port, --proxy-secret | MTProto proxy |
--proxy-pass | Open the SSH tunnel on start |
--sandbox | Die instead of restarting |
--no-tty | No colorful ANSI output |
Keeping it running
One line from the project directory: uv run -m ataraxia. Closing that terminal stops the process. On a VPS keep it alive in screen or tmux: start it inside a session, detach with Ctrl+A D, return with screen -r ataraxia.
Restarting from inside Telegram (.restart, .update) works via os.execvp("uv", ...): the process replaces itself, no external wrapper needed. Docker and Windows go through atexit.
Login and web UI
On first start ataraxia opens a web interface: QR or phone login, API credentials and 2FA, all in the browser.
API ID and API hash
- Sign in at my.telegram.org. Telegram sends the code to the app
- Open API development tools and create an application
- Copy App api_id (a number) and App api_hash (32 characters)
The web UI also offers ready-made presets for Telegram Desktop and Telegram for Android that fill in their own api_id and api_hash, and generate a plausible device profile. You can provide your own values via the api_id and api_hash environment variables or an api_token.py file; config.json wins once the values are saved there.
SecretDo not publish the API hash or paste it into screenshots.
QR or phone login
The web UI draws a QR code that refreshes as it expires; scan it from Telegram's device settings. Phone login takes the number in international format and then the code Telegram sends to your app. If two-step verification is enabled, the cloud password is requested in both the web UI and the terminal. The session is saved encrypted as ataraxia-<your_id>.atsession.
The web UI
Built with aiohttp and jinja2, templates in web-resources/. It handles API credential entry, code sending, QR login, 2FA and account management. --no-web disables it; login then happens in the terminal.
If the bot lives on a headless VPS, the .weburl command opens an SSH tunnel through serveo.net or localhost.run and returns a public https link to the UI. In groups the command asks for confirmation first, since the link exposes the UI to the chat. The tunnel can also be opened at startup with --proxy-pass.
Multiple accounts
ataraxia is multi-client: several sessions run in one process, each with its own database, modules and logs. Add an account from the web UI or with .addacc in Telegram: an inline form collects the number and the code, then the bot restarts with the new session. Logging in for someone else requires confirmation or a force_insecure suffix.
After login
- A private channel with a forum is created: Assets and Backups topics hold media and backups. Do not delete it
- The inline bot sends a welcome message with language buttons and a backup-period question
- The inline bot is pinned into the ataraxia chat folder, if you have one
CheckSend
.pingto Saved Messages. If the message turns into a card with latency and uptime, everything works.
Files, backups and migration
You can recreate Python dependencies with uv. The session, its key and the databases are what you protect and carry over.
| Path | What it holds | Migrate? |
|---|---|---|
config.json | API credentials and the web UI port | Yes, privately |
ataraxia.key | Session master key, 32 bytes hex, mode 0600 | Yes, apart from sessions |
ataraxia-<id>.atsession | Encrypted binary session, AES-256-GCM | Yes, with the key |
db-*.json | Per-account settings and module data | Yes |
loaded_modules/ | External modules and the .dlm cache | Yes, especially file-installed modules |
ataraxia-...-bot.atsession | The inline bot's session, same key | Yes |
.venv/, uv cache | Installed Python dependencies | No, uv recreates them |
The database writes atomically: temp file, fsync, then os.replace. Serialization is orjson; there is no stdlib json and no Redis.
Moving to another server
- Stop ataraxia on the old server so the same session is not active in two places
- Transfer the project together with
config.json,ataraxia.key, the.atsessionfiles, databases andloaded_modules/over a protected connection - Install uv on the new server and run
uv run -m ataraxia; dependencies resolve themselves - Send
.pingto Saved Messages and check.ataraxia: branch and commit should match the old server
.backupall saves the database, modules and pip requirements to the service forum. That archive does not replace the session or the key: migrating with only the backup means logging in again.
Finding backups
The private service forum has a Backups topic. .backupdb, .backupmods and .backupall send files there and return a link. .set_backup_period sets the automatic interval in hours; 0 disables it.
Do not share sessionsA session archive and the account database contain private data. Support usually needs an error log, not your
.atsessionorconfig.json.
The basics
A command is a message beginning with a prefix. The default prefix is a dot.
.help
.ping
.setlang ru
After the prefix comes the command name, then its arguments. In command help, <angle brackets> mean required input and [square brackets] mean optional input. Use the spelling shown by .help.
Escaping and layouts
A doubled prefix escapes the command: ..hello is sent as the plain text ".hello". The dispatcher also translates keyboard layouts: a command typed in the wrong layout is recognized anyway, so layout slips do not break commands.
Replies
Some commands take a message to work on from your reply. With .e, the replied-to message is available through r.
Change the prefix
.setprefix !
A single-character prefix works immediately; longer ones, emoji or premium emoji require the allow_nonstandart_prefixes setting. The prefix s is rejected. Personal prefixes exist too: .setprefix ! 123456789 assigns a prefix to a user who already has permissions (a co-owner, a security-group member or a targeted-rule target).
Aliases and disabling
.addalias p ping adds a short command .p. Check saved aliases with .aliases and remove one with .delalias p. .togglecmd name disables one command; .togglemod ModuleName toggles a whole module. Run the same toggle again to restore.
Toggles and grep
.settings opens the general toggles: NoNick (commands without mentioning your username in groups), Grep and more. With Grep enabled, command output can be filtered with | grep word.
Take care with code
.eruns Python and.terminalruns shell commands on the host machine. Neither is a sandbox for snippets someone sent you.
Everyday tasks
You do not need to memorize the command list. These are useful starting points.
Find a command
Send .help and choose a module. Flags narrow the list: .help -c built-in only, .help -l external only, -f shows hidden ones too.
Install a module
.dlm lists repository catalogs, .dlm name installs by name, and .dlm url installs from any URL: github/gitlab blob links are converted to raw automatically. For a Python file, upload it and reply with .lm. Check .help afterward.
Read an error
.logs opens a level picker; the level can also be passed by name or number. .logs debug -f sends the debug log without the private-data confirmation. Review any log before sharing it.
Update or disable something
Create a backup with .backupall before .update. The updater runs git pull in the current branch, then uv sync -U, showing a card with the current and upcoming commit. -f skips the confirmation. If the dependency sync fails, the code on disk is already new while the running one is old: the bot offers a retry instead of restarting. .restart restarts without updating. .togglemod ModuleName temporarily disables a module, and .watcherbl ModuleName disables its watcher in the current chat.
Give limited access
.tsec user @username ping 1h offers to allow that user to run ping for one hour. Use .security for broader settings. .owneradd grants much more access than is needed for one command.
Open the web UI from your phone
.weburl opens the SSH tunnel and returns the link. If the bot runs on a VPS and the only browser around is on your phone, that is the shortest path to the login forms and account management.
Command reference
Built-in commands of ataraxia. Third-party modules add their own; use .help ModuleName for those. Names in parentheses are aliases.
Core commands and settings
| Command | What it does |
|---|---|
.ataraxia | Shows the version, ataraxia-tl layer, branch and commit with a link |
.info | Bot info card: owner, uptime, CPU, RAM, OS. Text is customizable with placeholders |
.ping | Shows response time and uptime |
.setprefix <prefix> [id] | Changes the prefix; with an id or username, assigns a personal prefix |
.aliases | Lists your custom aliases |
.addalias <alias> <command> | Adds an alias, for example .addalias p ping |
.delalias <alias> | Removes an alias |
.settings | Opens the general toggles menu |
.togglecmd <command> | Disables or enables one command |
.togglemod <module> | Disables or enables a whole module |
.clearmodule <module> | Deletes that module's saved database data |
.cleardb | Clears the entire database after confirmation |
.setlang <lang> | Sets the interface language; en and ru ship built in |
.dllangpack <url> | Adds an external YAML language pack |
Help
| Command | What it does |
|---|---|
.help [module] | Lists modules or shows one module's commands. Flags: -c built-in, -l external, -f show hidden |
.helphide <module> | Hides or shows a module in the overall list |
.support | Sends support links |
Module loader
| Command | What it does |
|---|---|
.dlm (.dlmod) | Lists repository catalogs; with a name or URL installs that module. Several names can be passed separated by spaces |
.lm (.loadmod) | Loads a Python file attached to the command or to the message you replied to |
.ulm <class> (.unloadmod) | Unloads a module by class name; several can be listed separated by commas or newlines |
.clearmodules | Removes installed external modules after confirmation |
.addrepo <url> | Adds a repository with a non-empty full.txt |
.delrepo <url> | Removes a repository from the list |
.ml <module> | Sends that module's source file |
Downloaded code is cached in loaded_modules/.dlm_cache/<id>/ as a snapshot keyed by URL hash: the next install from the same address uses the snapshot, not the network. Repositories support basic auth via the loader's basic_auth setting. Packages from # packages: are installed through the system package manager when one is found.
Watchers and NoNick
| Command | What it does |
|---|---|
.watchers | Lists active and disabled message watchers |
.watcherbl <module> | Toggles that module's watcher in the current chat |
.watcher <module> | Toggles a watcher globally; -c, -p, -o, -i restrict chats, PM, outgoing or incoming messages |
.nonickuser, .nonickchat, .nonickcmd <command> | Toggle NoNick for a replied-to user, current group or command |
.nonickusers, .nonickchats, .nonickcmds | List the respective NoNick settings |
Updates
| Command | What it does |
|---|---|
.update | Updates ataraxia: git pull, then uv sync. Shows a card with the old and new commit |
.update -f | Updates without confirmation |
.restart | Restarts ataraxia; settings persist |
.source | Links to the source code |
Restarting from inside works via os.execvp("uv", ...): the process replaces itself. Docker and Windows go through atexit.
Code execution
| Command | Language | Requires |
|---|---|---|
.e (.eval) | Python | Nothing extra |
.ecpp | C++ | g++ |
.ec | C | same g++ |
.enode | JavaScript | node |
You can reply to a message containing code. The execution timeout is 10 seconds. See the full .e guide.
Terminal
| Command | What it does |
|---|---|
.terminal <command> (.exec) | Runs a shell command on the host. Output streams into the message as it arrives |
.terminate | Sends SIGTERM to the process group; reply to the message with its output |
.terminate -f | Force-kills the process (SIGKILL) |
If a command asks for a sudo password, the bot sends a form to Saved Messages: type the password there and it goes to the process's stdin. Wrong passwords and lockouts are detected and reported. The working directory is ataraxia's base directory.
Logs and diagnostics
| Command | What it does |
|---|---|
.logs [level] | Opens a log-level picker and sends the log as a file; the level can be a name or a number |
.logs debug -f | Sends the debug log without the privacy confirmation |
.clearlogs | Clears logging-handler buffers |
.suspend <seconds> | Pauses the process for the given time |
.api_fw_protection | Toggles the API rate limiter |
.suspend_api_protect <seconds> | Temporarily suspends the limiter |
The limiter watches call frequency: when a sampling window exceeds the threshold, it waits locally instead of catching a Telegram FloodWait. Individual methods like joinChannel and importChatInvite can be forbidden entirely via forbidden_methods; such requests are silently dropped from the call.
Backups
| Command | What it does |
|---|---|
.backupdb, .restoredb | Saves the database or restores it from a file you replied to |
.backupmods, .restoremods | Saves external modules or restores them from an archive |
.backupall, .restoreall | Saves or restores database, modules and pip requirements in one archive |
.set_backup_period <hours> | Sets automatic backup frequency; 0 disables, 199 hours max |
Access and security
| Command | What it does |
|---|---|
.security [command] | Configure access globally or for a particular command |
.inlinesec [command] | Configure access for an inline command |
.querysec | Toggles external inline-query access |
.owneradd, .ownerrm, .ownerlist | Add, remove or list co-owners |
.newsgroup, .sgroups, .sgroup, .delsgroup | Create, list, inspect or delete security groups |
.sgroupadd, .sgroupdel | Add or remove a user in a security group |
.tsec, .tsecrm, .tsecclr | List, grant or remove targeted rules for users, chats and security groups |
For example, .tsec user @username ping 1h offers to allow ping for an hour. Rules cover commands, whole modules and inline handlers.
Inline bot, web and accounts
| Command | What it does |
|---|---|
.ch_ataraxia_bot <username> | Changes the inline bot's username; generates a random one when omitted |
.ch_bot_token <token> | Uses an existing bot via its token |
.weburl | Opens the SSH tunnel and links the web UI |
.addacc <user> | Starts adding another Telegram account through an inline form |
.config (.cfg) | Opens module and library configuration menus |
.fconfig (.fcfg) | Sets values directly: .fconfig Module option value, changes joined with &&. Without a value it takes the reply's text |
Security and permissions
Every command carries a bitmask of who may run it. Inline buttons inherit the security of the command that created them.
The bitmask
The owner always passes. Then bits add up: a security group contributes its own, chat rights grant the GROUP_* bits, a private chat with the account grants PM, and EVERYONE opens the command to all. By default a command has only OWNER. In .security, toggles switch bits off: a bit disabled globally is disabled for every command.
| Bit | Who gets it |
|---|---|
OWNER | Owner and co-owners in the owner group |
GROUP_OWNER | Group creator |
GROUP_ADMIN_* | Group admins per right: add members, change info, ban, delete, pin, invite |
GROUP_ADMIN | Any group admin |
GROUP_MEMBER | Any group member |
PM | Anyone in a private chat with the account |
EVERYONE | Everyone, including inline queries |
Modules do the same with decorators: @loader.owner, @loader.group_admin, @loader.pm, @loader.unrestricted, plus one per admin right.
Targeted rules and groups
.tsec user @username ping 1h offers to grant one person the ping command for an hour. Rules can target a command, a module or an inline handler, and the subject can be a user, a chat or a security group. .tsec without arguments lists active rules. Security groups are named user lists with permission sets: .newsgroup, .sgroupadd, .sgroupdel.
Inline bot and buttons
The inline bot serves forms and buttons, and it is a second MTProto client: its session ataraxia-<id>-bot.atsession sits next to yours under the same key. The bot is created through @BotFather on first start unless you supply a token. .config menus, confirmations and paginated lists run through it.
Form buttons inherit the security of the calling command. A module can change that with force_me (owner only), always_allow (an explicit allow-list) or disable_security (no checks). Forms live for a day by default, after which callback buttons stop working; url buttons keep working. .querysec toggles whether strangers can use inline queries.
Restrictions on external modules
There is no sandbox, but a few guardrails exist. A module with allow_paid_stars = False cannot send paid star requests: the attempt raises PermissionError. Importing aiogram is rejected at load time: the inline bot already exists as self.inline, and aiogram is never installed from # requires:. Imports of telethon and herokutl are redirected to ataraxia-tl, so older code often works unchanged, but test it anyway.
FloodWait protection
The dispatcher counts commands per user and per chat over time; over the limit, the command is not executed. The owner is exempt. The API limiter additionally throttles MTProto requests themselves: the local_floodwait setting decides how many seconds to wait locally when a FloodWait threatens, up to 3600.
Co-owner access
.owneraddgrants broad permissions. For one or two commands, a targeted.tsecrule is usually a better fit.
.e: Python in chat
Write a Python expression after .e. ataraxia evaluates it using meval inside the running userbot process and sends back the result.
.e 2 + 2
.e "hello".upper()
.e await client.get_me()
.e print("hello from Python")
Top-level await works, print() output is captured, and ataraxia displays execution time. The code is real Python running with ataraxia's permissions, not a sandbox.
Names available inside .e
| Name | Meaning |
|---|---|
message, m, event | Your command message |
r, reply | The message you replied to, or None |
client, c | Telegram client |
chat | The command message's destination (message.to_id) |
db, lookup, self | Database, module lookup and Executor instance |
utils, loader, main | ataraxia helpers |
ataraxiatl, telethon, herokutl | Three names for the same library |
Telegram TL functions and types are exposed under their class names, loaded lazily for the ones your code mentions. The db variable is wrapped during execution: db.set("ataraxia.security", "owner", ...) is forbidden, so the owner cannot be changed through eval.
What is r?
r is the same object as reply, returned by await message.get_reply_message(). Reply to a message and send .e r.text. With no reply, both names are None, so r.text raises an error.
The reference below was checked against the ataraxia-tl Message class (a Telethon fork). Properties are read without await; async methods require it.
Text, sender and chat
| Property on r | Meaning |
|---|---|
text, raw_text, message | Formatted text under the client's parse mode, and raw text. Raw text may be None on service messages |
entities | Formatting entities such as links, bold and code |
id | Message ID. Use r.id, not r.message_id |
date, edit_date | Sent and last edited timestamps |
sender_id, sender, input_sender, from_id | Sender ID, cached sender, API input sender and raw sender peer |
chat_id, chat, input_chat, peer_id, to_id | Chat ID, cached chat and peer variants for Telegram requests |
is_private, is_group, is_channel | Chat type; supergroups can be both groups and channels in the API |
out, post, mentioned | Outgoing flag, channel-post flag and whether you were mentioned |
is_reply, reply_to_msg_id, reply_to | Reply flag, original message ID and raw reply header |
reply_to_chat, reply_to_sender | Original chat/sender for cross-chat replies when available |
r.sender and r.chat can be empty if they are not cached. await r.get_sender() and await r.get_chat() can fetch them, though they too may return None.
Media, files and buttons
| Property on r | Meaning |
|---|---|
media, file | Raw media and a convenient File wrapper. file may be None |
photo, document | Photo or document when present |
video, video_note, gif | Video, round video or animation |
audio, voice, sticker | Audio, voice note or sticker |
web_preview, contact, geo | Web preview, contact or coordinates |
venue, poll, dice, game, invoice | Place, poll, dice roll, game or invoice |
buttons, button_count, reply_markup | Buttons by row, total count and raw markup |
media_unread, grouped_id | Media unread flag and album/group ID |
When r.file exists, it may have name, ext, mime_type, size, width, height, duration, title, performer, emoji, sticker_set and id. An image will not have a song title.
Forwards, reactions and raw fields
forward and fwd_from describe forwarding. views, forwards and replies hold counts or reply data if Telegram supplied them. reactions, pinned, post_author, silent, noforwards, via_bot_id, via_input_bot, ttl_period and restriction_reason are available when relevant. Service messages can contain action and action_entities. More raw fields such as saved_peer_id, from_scheduled, legacy, edit_hide, invert_media, offline, effect, factcheck, suggested_post and rich_message depend on the message type and ataraxia-tl version.
Message methods
Every method in this table needs await except get_entities_text().
| Method on r | What it does |
|---|---|
get_sender(), get_input_sender() | Gets the sender or an API input peer, fetching data when necessary |
get_chat(), get_input_chat() | Gets the chat or an API input peer |
get_reply_message() | Gets the message that r itself replies to |
respond("text"), reply("text") | Sends to the chat; reply replies directly to r |
forward_to("me") | Forwards the message to Saved Messages |
edit("text"), delete() | Edits or deletes if Telegram permissions allow it |
download_media() | Downloads attached media to the host |
get_buttons(), click(0), click(text="Yes") | Retrieves buttons or clicks one; indices start at zero |
mark_read() | Marks the message as read |
pin(), unpin() | Pins or unpins where permitted |
react("👍") | Sends a reaction when permitted |
link() | Builds a message link where supported |
translate("en") | Returns a translation through the client method where available |
get_entities_text() | Synchronous: returns pairs of formatting entity and matching text |
click also accepts row and column, filter or callback data. Buttons requesting your phone, location or 2FA password need explicit arguments. Do not click buttons in untrusted messages blindly.
Examples and common mistakes
.e len(r.raw_text or "")
.e len((r.raw_text or "").split())
.e r.file.name if r.file else "No file"
.e (await r.get_sender()).first_name
.e await r.reply("Got it")
.e await r.forward_to("me")
.e await r.link()
.e await r.react("👍")
.e await r.download_media()
r.raw_text may be None, so the counting examples use or "". r.file.name fails without an attached file. Even get_sender() may return None. You cannot edit someone else's message merely because you obtained it through a reply.
Sending bare .e as a reply to a message containing code makes the evaluator run the replied-to text. Read it first.
Output and other languages
The last expression's value is shown automatically; print() output is captured and shown alongside the execution time. Errors come back as a traceback: the last line usually names the cause. Phone numbers, session strings, tokens and keys are masked in output; that is a safety net, not a reason to print secrets. Long output becomes a paginated inline form or a file.
For other languages use .ecpp (C++), .ec (C) or .enode (JavaScript). Their compilers or runtimes must be installed separately. They do not provide the Python variable r.
Rich messages
Telegram can render rich messages: headings, tables, blockquotes, expandable blocks. ataraxia sends them two ways: markdown from your account and HTML screens through the inline bot.
Two paths, one goal
| Way | Sent as | What you get | Who can use it |
|---|---|---|---|
rich="markdown" | Your account | Headings, tables, nested lists, code blocks | Everyone, no Premium needed |
Screen | The inline bot | All of that plus in-text buttons and a bottom keyboard | Everyone; custom emoji require Premium |
Telegram renders both itself: you send the source, the client draws it. If the chat or client rejects the format, ataraxia falls back to plain text with buttons at the bottom on its own; the message is not lost.
Account markdown
The simplest route is utils.answer: its rich parameter takes a markdown source, and the message goes out from your account, not the bot.
await utils.answer(
message,
rich=(
"# Report\n"
"| Metric | Value |\n"
"|---|---|\n"
"| Ping | 42 ms |\n"
"| Uptime | 3 days |\n\n"
"- **CPU**: 12%\n"
"- **RAM**: 210 MB\n"
),
)
The markdown= parameter of utils.answer is the same thing as rich=. The difference only matters inside inline forms: there markdown is sent by the bot, while rich comes from your account.
Supported markdown: # headings, tables, nested lists, code blocks, quotes. Telegram turns the source into a rich message itself, so the text is never parsed or split into pages: what you wrote goes out whole.
The Screen API
When you need buttons inside the text, build a screen. Screen is a rich HTML constructor: the body is assembled from fragments, and in-text taps register as ordinary form callbacks with the same security checks.
from ..inline.screens import Screen
def build_screen(self) -> Screen:
k = self.inline.screen() # aware of the owner's Premium
k.add(
f"{k.icon('settings')} Menu {k.badge('3 items', 'primary')}",
k.quote("Menu description"),
)
k.add(
k.details(
f"{k.icon('module')} Sections · 2",
"\n".join(
k.tap(name, self.section_cb, name) + f" {k.badge(n)}"
for name, n in [("Main", 5), ("Other", 2)]
),
)
)
k.add(
k.table([
["Option", "Value"],
["Language", "en"],
["Prefix", "."],
])
)
k.keyboard(
[k.input("Add", self.add_input, "Add what?", icon="add")],
k.footer(self.back_home),
)
return k
@loader.command()
async def menucmd(self, message):
"""Open menu"""
await self.inline.form(message=message, rich_html=self.build_screen())
Body fragments
| Method | What it draws |
|---|---|
k.tap(label, cb, *args) | An in-text button; the callback receives call and the args. Styles: primary, success, danger, link |
k.copy(label, value) | A copy button, values up to 256 characters |
k.badge(text, style) | A disabled badge, e.g. a counter |
k.row(*buttons, align=) | A row of several buttons, up to 8 per row |
k.details(summary, body, open=) | An expandable block |
k.table(rows, keys=) | A table; the first row is the header by default |
k.quote(body, expandable=) | A blockquote, plain or expandable |
k.image(url) | An image |
k.icon("settings") | An icon: custom emoji with Premium, a plain one without |
Bottom keyboard and input
| Method | What it does |
|---|---|
k.button(label, cb, *args) | An ordinary bottom button |
k.input(label, handler, prompt) | A button asking for a value: handler(call, value) |
k.copy_button(label, value) | A bottom copy button |
k.back(cb), k.close() | "Back" and "Close" |
k.footer(back_cb) | A ready "Back + Close" row |
k.pager(cb, page, pages) | "‹ 1 / N ›"; cb(call, page=...). Pages are cut by k.page(items, page, size) |
k.reply(handler) | Lets the user reply to the screen message with text: handler(call, text). The reply is deleted |
A screen is edited like any form: await call.edit(rich_html=another_screen). The helper await self.inline.show(call, screen) does that and also shows a toast: set screen.toast = "Saved" before showing, and screen.alert = True turns it into a popup. For a warning in the body there is k.warn(text).
Plain fallback
An old client, a chat without support or a send error does not break the message. When a rich form fails, ataraxia converts the screen into ordinary Telegram HTML: tables become "key: value" lines, in-text taps move to the bottom keyboard, expandable blocks unfold. The same happens in utils.answer: the markdown goes out as is, and on error it is retried as plain text.
One caveat: rich HTML cannot be combined with media in one form; photo, gif, file, video and the rest do not mix with rich_html. An image inside a rich message is added with an <img> tag in the HTML itself.
Writing a module
A module is a Python file defining a class derived from loader.Module. Upload it to Telegram and reply with .lm, or drop it into loaded_modules/.
A minimal command
from ataraxia import loader, utils
@loader.tds
class HelloMod(loader.Module):
"""A small greeting module"""
strings = {"name": "Hello"}
@loader.command()
async def hello(self, message):
"""Send a greeting"""
await utils.answer(message, "Hello")
The command becomes .hello. The method docstring is shown in help. Alternatively, name the method hellocmd, whose cmd suffix is removed from the command name.
Arguments and replies
utils.get_args_raw(message) returns the text after the command. utils.get_args(message) splits it into arguments. await message.get_reply_message() retrieves the message being replied to. await utils.answer(message, rich="markdown") sends a rich message from your account: headings, tables and nested lists, falling back to plain text.
Configuration and saved data
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
"greeting", "Hello", "Text to send",
validator=loader.validators.String(),
),
)
This appears in .config and can be read with self.config["greeting"]. Validators include Boolean, Integer, Float, Choice, MultiChoice, Series, Link, Hidden and more. For module-owned persistent data use self.get("key", default) and self.set("key", value); self.pointer returns an auto-saving list or dict wrapper.
Watching messages and scheduling work
@loader.watcher("in", "only_messages", "no_commands")
async def watcher(self, message):
if "hello" in (message.raw_text or "").lower():
await message.reply("Hello")
@loader.loop(interval=300, autostart=True)
async def ticker(self):
await self.client.send_message("me", "Five minutes passed")
The watcher tags restrict it to incoming messages, excluding service events and userbot commands. The full tag set is wider: out, only_media, no_pm, only_groups, regex, contains, startswith, from_id, chat_id and more, combinable directly inside @loader.watcher(...). A loop's interval is measured in seconds. A watcher replying to every message can quickly hit Telegram rate limits, so narrow its conditions.
For raw TL updates there is @loader.raw_handler(UpdateType): reactions, boosts, business events and the rest arrive as raw objects, and the handler is unregistered when the module unloads, leaving no zombie handlers.
Buttons
async def pressed(self, call):
await call.answer("Done")
await call.edit("Button pressed")
@loader.command()
async def menucmd(self, message):
"""Open menu"""
await self.inline.form(
text="Menu",
message=message,
reply_markup=[
{"text": "Press", "callback": self.pressed},
{"text": "Close", "action": "close"},
],
)
Callbacks receive call with answer(), edit(), delete() and unload access. Button keys include callback, url, input, data, web_app, copy, ready-made actions like close, plus style (primary, success, danger, link) and emoji_id. Paginated lists use self.inline.list(...).
Lifecycle
Use client_ready after the Telegram client becomes available, config_complete after configuration loads and on_unload for cleanup. A loader.LoadError("reason") raised during loading tells the user why the module cannot run; loader.SelfUnload quietly unloads it when a condition is not met.
What lives on self
| Name | Purpose |
|---|---|
self.client | Telegram client |
self.db | Shared module database |
self.get, self.set, self.pointer | Module data; pointer auto-saves list and dict changes |
self.inline | Forms and lists |
self.inline.bot | Bot API shaped facade over the MTProto inline bot |
self.lookup("Name") | Another loaded module |
self.tg_id | Current account's Telegram id |
self.get_prefix() | Current command prefix |
self.import_lib(url) | Load an external library with a loader.Library class |
The inline bot as Bot API
self.inline.bot is a Bot API shaped facade: send_message, edit_message_text, send_photo, send_media_group, answer_inline_query, answer_callback_query, set_my_commands, get_chat_member, get_file and more. Under the hood it issues real MTProto requests as the bot, so download_file has no size limits. Anything the facade lacks goes through await self.inline.bot.invoke(functions.SomeRequest(...)). It accepts and returns plain objects, dicts work too, and errors come as TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter with retry_after, all carrying the facade method name.
Guest commands
A guest command is invoked as @botusername command args from any chat, even ones the bot is not in (Bot API guest mode). Mark it with @loader.guest_command() or name it <cmd>_guest_handler; it receives (message, args) and answers via self.inline.answer_guest(message, ...):
@loader.guest_command()
async def ask(self, message, args):
await self.inline.answer_guest(message, "from a chat the bot is not in")
Only the owner or members of the owner group may invoke guest commands. A bare mention or an unknown command is answered with silence. No guest commands ship built in; this is an external-module feature. The answer returns an editable InlineMessage. Buttons in guest chats are open to everyone by default unless you pass force_me or always_allow.
File metadata
# meta developer: @username
# requires: requests
# packages: ffmpeg
# scope: ataraxia 0.1.0
__version__ = (1, 0, 0)
The loader reads these lines to display the developer, install a uv dependency, install a system package, and enforce a minimum ataraxia version. The special scopes # scope: ffmpeg and # scope: inline require ffmpeg and a configured inline bot. Imports of telethon and herokutl are redirected to ataraxia-tl; test older modules anyway. For working examples, read ataraxia/modules/translations.py and ataraxia/modules/test.py.
For translations a module can define strings_ru and other suffixes: the right variant is picked by the current language, with the common strings as the fallback. Shared libraries load via self.import_lib(url) with a loader.Library subclass whose name ends in Lib. Helpers like utils.escape_html and utils.get_chat_id live in ataraxia/utils/.
Troubleshooting
Start with terminal output and .logs. The last line of an error often identifies the problem.
ataraxia-tl is required, use uv sync
Dependencies are unresolved. Run uv sync in the project directory, then uv run -m ataraxia. If uv is missing, install it: curl -LsSf https://astral.sh/uv/install.sh | sh.
Running it as a script fails
ataraxia cannot be run as a file. Run it as a package: uv run -m ataraxia from the repository root.
SessionDecryptError or SessionKeyMissing
The ataraxia.key file is missing or does not match the sessions. Sessions are useless without the key: you will have to log in again. Check whether you copied the project without the key.
The web UI does not open
The default port is random; see the startup banner or set your own with --port. On a VPS use .weburl or --proxy-pass: the tunnel gives you a public https link. --no-web disables the web UI entirely.
Telegram reports FloodWait
Wait for the period stated in the error before trying again. Repeated login attempts may make it longer. The .api_fw_protection limiter waits locally and prevents FloodWait from happening.
The inline bot was not created
@BotFather may limit new bots. You can use an existing bot via .ch_ataraxia_bot <name> and .ch_bot_token <token>. Keep the token private.
Commands do not respond
Check that the process is running and the command comes from the authorized account. In groups, commands require mentioning your username by default: disable that with NoNick (.nonickchat in that chat). Also check your prefix and chat blacklists.
A module will not load
Open .logs for the cause: missing dependency, incompatible ataraxia version, module code error or an unavailable inline bot. Modules importing aiogram are rejected outright: use self.inline. Try disabling the failing module before resetting the installation.
An update failed on dependencies
git pull already went through, so the code on disk is new while the running one is old. The bot will not restart and offers a retry button. Check the error output: usually the network or git.break.wtf being unreachable for ataraxia-tl.
I want to start from scratch
Save a backup first. .cleardb clears only the database; the session and key files are removed manually from the project directory, after which the next start offers a fresh login.
Getting help
Project channel: @ataraxia_ub, chat: @ataraxia_talks. .support sends the same links. Review logs for private data before posting them in a public chat.