ataraxia
ataraxia Handbookdev-oriented Telegram userbot
codename Rise · Python 3.10+ · AGPLv3
RUEN
ataraxia

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 cmd from 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.toml is 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 rule

Sessions are encrypted with AES-256-GCM, but ataraxia.key sits 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.

HostBest forNote
Linux VPS24/7 uptimeFrom a couple of dollars, needs ssh
Home PCTrying things outWorks while powered on
Raspberry PiQuiet always-on hostNeeds a separate device
DockerContainer setupsData in the /data volume, port 8080

Linux and macOS

terminal
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

terminal
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

FlagWhat it does
--portWeb UI port. Random free port by default, always 8080 in Docker
--no-webDisable the web UI, log in from the terminal
--data-rootRoot path for session and data files
--phone, -pPhone number to log in; can be passed multiple times
--no-authSkip authentication and API token input, exit if needed
--proxy-host, --proxy-port, --proxy-secretMTProto proxy
--proxy-passOpen the SSH tunnel on start
--sandboxDie instead of restarting
--no-ttyNo 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

  1. Sign in at my.telegram.org. Telegram sends the code to the app
  2. Open API development tools and create an application
  3. 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.

Secret

Do 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
Check

Send .ping to 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.

PathWhat it holdsMigrate?
config.jsonAPI credentials and the web UI portYes, privately
ataraxia.keySession master key, 32 bytes hex, mode 0600Yes, apart from sessions
ataraxia-<id>.atsessionEncrypted binary session, AES-256-GCMYes, with the key
db-*.jsonPer-account settings and module dataYes
loaded_modules/External modules and the .dlm cacheYes, especially file-installed modules
ataraxia-...-bot.atsessionThe inline bot's session, same keyYes
.venv/, uv cacheInstalled Python dependenciesNo, 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

  1. Stop ataraxia on the old server so the same session is not active in two places
  2. Transfer the project together with config.json, ataraxia.key, the .atsession files, databases and loaded_modules/ over a protected connection
  3. Install uv on the new server and run uv run -m ataraxia; dependencies resolve themselves
  4. Send .ping to 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 sessions

A session archive and the account database contain private data. Support usually needs an error log, not your .atsession or config.json.

The basics

A command is a message beginning with a prefix. The default prefix is a dot.

in any chat
.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

in Telegram
.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

.e runs Python and .terminal runs 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

CommandWhat it does
.ataraxiaShows the version, ataraxia-tl layer, branch and commit with a link
.infoBot info card: owner, uptime, CPU, RAM, OS. Text is customizable with placeholders
.pingShows response time and uptime
.setprefix <prefix> [id]Changes the prefix; with an id or username, assigns a personal prefix
.aliasesLists your custom aliases
.addalias <alias> <command>Adds an alias, for example .addalias p ping
.delalias <alias>Removes an alias
.settingsOpens 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
.cleardbClears 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

CommandWhat 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
.supportSends support links

Module loader

CommandWhat 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
.clearmodulesRemoves 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

CommandWhat it does
.watchersLists 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, .nonickcmdsList the respective NoNick settings

Updates

CommandWhat it does
.updateUpdates ataraxia: git pull, then uv sync. Shows a card with the old and new commit
.update -fUpdates without confirmation
.restartRestarts ataraxia; settings persist
.sourceLinks to the source code

Restarting from inside works via os.execvp("uv", ...): the process replaces itself. Docker and Windows go through atexit.

Code execution

CommandLanguageRequires
.e (.eval)PythonNothing extra
.ecppC++g++
.ecCsame g++
.enodeJavaScriptnode

You can reply to a message containing code. The execution timeout is 10 seconds. See the full .e guide.

Terminal

CommandWhat it does
.terminal <command> (.exec)Runs a shell command on the host. Output streams into the message as it arrives
.terminateSends SIGTERM to the process group; reply to the message with its output
.terminate -fForce-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

CommandWhat 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 -fSends the debug log without the privacy confirmation
.clearlogsClears logging-handler buffers
.suspend <seconds>Pauses the process for the given time
.api_fw_protectionToggles 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

CommandWhat it does
.backupdb, .restoredbSaves the database or restores it from a file you replied to
.backupmods, .restoremodsSaves external modules or restores them from an archive
.backupall, .restoreallSaves 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

CommandWhat it does
.security [command]Configure access globally or for a particular command
.inlinesec [command]Configure access for an inline command
.querysecToggles external inline-query access
.owneradd, .ownerrm, .ownerlistAdd, remove or list co-owners
.newsgroup, .sgroups, .sgroup, .delsgroupCreate, list, inspect or delete security groups
.sgroupadd, .sgroupdelAdd or remove a user in a security group
.tsec, .tsecrm, .tsecclrList, 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

CommandWhat 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
.weburlOpens 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.

BitWho gets it
OWNEROwner and co-owners in the owner group
GROUP_OWNERGroup creator
GROUP_ADMIN_*Group admins per right: add members, change info, ban, delete, pin, invite
GROUP_ADMINAny group admin
GROUP_MEMBERAny group member
PMAnyone in a private chat with the account
EVERYONEEveryone, 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

.owneradd grants broad permissions. For one or two commands, a targeted .tsec rule 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.

Saved Messages
.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

NameMeaning
message, m, eventYour command message
r, replyThe message you replied to, or None
client, cTelegram client
chatThe command message's destination (message.to_id)
db, lookup, selfDatabase, module lookup and Executor instance
utils, loader, mainataraxia helpers
ataraxiatl, telethon, herokutlThree 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 rMeaning
text, raw_text, messageFormatted text under the client's parse mode, and raw text. Raw text may be None on service messages
entitiesFormatting entities such as links, bold and code
idMessage ID. Use r.id, not r.message_id
date, edit_dateSent and last edited timestamps
sender_id, sender, input_sender, from_idSender ID, cached sender, API input sender and raw sender peer
chat_id, chat, input_chat, peer_id, to_idChat ID, cached chat and peer variants for Telegram requests
is_private, is_group, is_channelChat type; supergroups can be both groups and channels in the API
out, post, mentionedOutgoing flag, channel-post flag and whether you were mentioned
is_reply, reply_to_msg_id, reply_toReply flag, original message ID and raw reply header
reply_to_chat, reply_to_senderOriginal 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 rMeaning
media, fileRaw media and a convenient File wrapper. file may be None
photo, documentPhoto or document when present
video, video_note, gifVideo, round video or animation
audio, voice, stickerAudio, voice note or sticker
web_preview, contact, geoWeb preview, contact or coordinates
venue, poll, dice, game, invoicePlace, poll, dice roll, game or invoice
buttons, button_count, reply_markupButtons by row, total count and raw markup
media_unread, grouped_idMedia 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 rWhat 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

reply to the target message
.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

WaySent asWhat you getWho can use it
rich="markdown"Your accountHeadings, tables, nested lists, code blocksEveryone, no Premium needed
ScreenThe inline botAll of that plus in-text buttons and a bottom keyboardEveryone; 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.

in a module or in .e
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.

inside a module class
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

MethodWhat 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

MethodWhat 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

hello.py
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

inside a module class
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

inside a module class
@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

inside a module class
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

NamePurpose
self.clientTelegram client
self.dbShared module database
self.get, self.set, self.pointerModule data; pointer auto-saves list and dict changes
self.inlineForms and lists
self.inline.botBot API shaped facade over the MTProto inline bot
self.lookup("Name")Another loaded module
self.tg_idCurrent 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, ...):

inside a module class
@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

at the top of a module file
# 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.