Как я написал свой асинхронный Telegram‑юзербот с «горячей» загрузкой плагинов и ассистентом, спроектированным в Word
Зачем миру еще один юзербот, или Как начать всё заново после провала
Начать хочется с честного признания: Kaguya UserBot — это моя вторая попытка. Первая закончилась полным архитектурным тупиком. Система получилась настолько негибкой, что в какой‑то момент я просто забросил проект на два месяца, а затем полностью удалил код и начал всё с чистого листа.
Зачем вообще писать свой юзербот, когда вокруг полно готовых решений вроде Hikka, FTG или Dragon‑Userbot? У меня было две основные причины:
Профессиональный вызов: Доказать самому себе, что я могу создать законченный, интересный и уникальный инструмент в open source.
Практическая польза: Мне, как разработчику, часто нужно быстро получать Telegram ID клиентов, парсить данные и использовать мелкие утилиты.
Но главная идея родилась из повседневного сценария. Представьте: вы находитесь в дороге, у вас с собой только смартфон, доступа к домашнему ПК или рабочему серверу нет. И тут вам срочно требуется новый инструмент — например, специфический калькулятор или переводчик. Что делать? Обновлять репозиторий через Git со смартфона неудобно, пересобирать проект — долго.
Правильное решение — динамическое подключение модулей «на лету». Прямо со смартфона можно зайти к любому ИИ (ChatGPT/Claude/Gemini), попросить набросать простой плагин по шаблону Кагуи, скопировать код, отправить его файлом в Telegram и ответить командой .установить (или .install). Бот сам проанализирует код, загрузит его в память и сразу же предоставит новую команду. Без перезапусков, без Git и без боли (просто сказка!).
Именно вокруг этой концепции — максимального удобства для мобильного использования и простоты для обычного пользователя — и начала строиться Kaguya.
Почему Kurigram, а не Pyrogram или Telethon?
Я уже около 2–3 лет работаю с библиотекой kurigram и полностью ей доверяю. Это стабильный, удобный и, главное, активно обновляемый форк классического Pyrogram.
Оригинальный Pyrogram, увы, заброшен разработчиками. Когда я только начинал свой путь в Telegram‑разработке, я пробовал Telebot, но быстро перерос его. Смотрел в сторону aiogram, но на тот момент мне не понравился его синтаксис и кажущаяся сложность настройки роутеров. Перешел на Pyrogram.
В процессе работы над моим большим проектом «Микуся» я столкнулся с тем, что Pyrogram не поддерживал стилизацию текстов цитатами (expandable blockquote), которые я очень люблю использовать для оформления заголовков. Тогда я и открыл для себя kurigram. У него активное и дружелюбное комьюнити, поддержка параллельной работы юзерботов и ботов через MTProto, а также быстрая реакция на изменения в API Telegram. Что касается Telethon — его синтаксис показался мне излишне сложным, а обновления выходят не так часто.
Одержимость оптимизацией: асинхронный Diskcache против JSON и Redis
Когда я работал над оптимизацией своего основного проекта — «Микуся» (это огромный комбайн, который обрабатывает медиа, музыку, шифрует файлы по AES-256, содержит собственную RPG‑игру «Проклятия», квесты и ИИ‑генераторы), мне потребовалось быстрое решение для кэширования статичных игровых и системных данных, чтобы разгрузить основную базу.
Поднимать полноценный Redis для этой задачи не хотелось, а использовать обычные файлы — хрупко. Идеальным кандидатом стал diskcache — библиотека для быстрого Key‑Value кэширования на базе SQLite, работающая со скоростью O(1).
Поскольку «Микуся» постоянно обрабатывает большие потоки данных, я сразу обратил внимание, что diskcache синхронен. Чтобы пресечь любые микро‑фризы и блокировки асинхронного цикла событий (Event Loop) Pyrogram, я превентивно написал асинхронную обертку AsyncCache на базе asyncio.to_thread:
class AsyncCache:
"""Асинхронное хранилище на базе diskcache."""
def __init__(self, directory: str):
self.directory = directory
self._cache = Cache(directory)
async def get(self, key: Any, default: Any = None) -> Any:
return await asyncio.to_thread(self._cache.get, key, default=default)
async def set(self, key: Any, value: Any, expire: Optional[float] = None) -> bool:
return await asyncio.to_thread(self._cache.set, key, value, expire=expire)Этот опыт очень помог мне при проектировании Kaguya UserBot. В отличие от монструозной «Микуси», Kaguya изначально задумывалась как ультралегкий юзербот, способный запускаться даже на смартфонах через Termux.
Заставлять мобильного пользователя разворачивать сервер Redis для хранения настроек — это безумие. Хранить данные в хрупких .json файлах, которые легко ломаются при конкурентной записи — тоже плохой путь. И вот тут связка SQLite‑хранилища diskcache и моей асинхронной обертки AsyncCache вписалась идеально: она разворачивается за секунды без внешних зависимостей и не забивает оперативную память смартфона.
Магия инлайн‑кнопок: заставляем юзербота делать невозможное
По правилам Telegram, обычные пользователи (и юзерботы) не могут отправлять инлайн‑клавиатуры в чаты. Это умеют делать только боты через Bot API. Но как обойти это ограничение?
Я реализовал гибридную схему, запустив в одном процессе юзербота и его бота‑ассистента. Они работают параллельно и делят общую базу данных. Когда пользователю нужно отправить сообщение с кнопками, происходит следующая магия:
Пользователь пишет команду:
.kb
[ Привет Хабр! | https://habr.com ]
Текст сообщенияЮзербот перехватывает сообщение, парсит структуру кнопок и текст.
Поскольку юзербот и бот‑ассистент живут в разных областях видимости API, передать данные напрямую сложно. Поэтому юзербот генерирует случайный UUID и сохраняет структуру сообщения в кэш
diskcache.Юзербот мгновенно удаляет исходное сообщение.
Юзербот инициирует скрытый инлайн‑запрос (Inline Query) к своему боту‑ассистенту, передавая UUID:
kb_<UUID>.Бот‑ассистент принимает инлайн‑запрос, считывает данные из общего
diskcache, формируетInlineQueryResultсо встроенной клавиатурой и отдает его юзерботу.Юзербот отправляет этот результат в чат от имени пользователя.

На выходе мы получаем бесшовный опыт: вы написали команду, она исчезла, и вместо нее появилось красивое сообщение с кнопками, отправленное лично вами. Но такой метод не поддерживает премиум эмодзи на уровне Telegram API.
Психология ошибок, или Почему Microsoft Word — это новая Figma
Изначально я надеялся, что все пользователи будут строго следовать текстовой инструкции по созданию и привязке бота‑ассистента. Но золотое правило разработки гласит: «Если разработчик где‑то позабыл — пользователь гарантированно забудет сделать нужное действие со 100% вероятностью».
Самая частая проблема: пользователь привязывает токен бота через команду .token, но забывает включить в настройках @BotFather инлайн‑режим. Юзербот перехватывает ошибку BotInlineDisabled, но как сообщить об этом пользователю?
Красный текст об ошибке вызывает тревогу и отторжение. Я решил сделать дружелюбный визуальный баннер‑инструкцию:
Пытался сгенерировать его через ИИ — выходило красиво, но с кривыми буквами и артефактами в интерфейсе Telegram.
Фотошоп показался мне слишком неудобным для быстрой верстки.
В итоге я взял инструмент, в котором за свою жизнь оформил сотни отчетов и таблиц — Microsoft Word! Простые геометрические фигуры, скриншоты, наложение неонового свечения на стрелочки — и баннер готов.

Когда у пользователя возникает ошибка, Kaguya присылает эту картинку прямо в чат. Визуальная информация воспринимается легче. Ошибка перестает быть проблемой и превращается в простой пятишаговый квест.
«Микро‑нано антивирус» и особенности распаковки ZIP‑архивов
Когда я увидел, как круто плагины устанавливаются на лету прямо из чата Telegram, ко мне быстро пришла критическая мысль: плагин — это исполняемый Python‑код. А значит, он имеет полный доступ к файловой системе, переменным окружения и .session‑файлам. Любой злоумышленник может написать скрытый стилер сессий.
Поскольку навыков в создании полноценных антивирусных систем у меня нет, я написал простейший статический анализатор кода против «глупых вредоносов»:
def check_security(code: str) -> list[str]:
"""Проверяет код на наличие потенциально опасных конструкций."""
suspicious_keywords = {
'eval(', 'exec(', '__import__', 'os.system', 'subprocess',
'.session', 'session_path'
}
normalized_code = ''.join(code.split()).lower()
...Конечно, обфусцированный или закодированный в base64 вредоносный код эта проверка пропустит, но от базовых попыток прочитать сессию она защищает. Чтобы обезопасить пользователей по‑настоящему, я создал отдельный официальный репозиторий с верифицированными плагинами kaguya‑modules, а в перспективе планирую сделать внутренний магазин плагинов прямо в ядре.
При реализации пакетной системы модулей (когда плагин устанавливается из .zip-архива) я столкнулся с тем, что программисты (включая меня) часто путаются в упаковке ZIP. Кто‑то архивирует файлы напрямую, а кто‑то — папку с файлами, из‑за чего внутри архива получается структура KaguyaPlugin/KaguyaPlugin/.
Чтобы система не ломалась из‑за отсутствия _init_.py на верхнем уровне, пришлось написать парсер структуры архива:
init_at_root = os.path.exists(os.path.join(temp_zip_dir, '__init__.py'))
subdirs = [d for d in os.listdir(temp_zip_dir) if os.path.isdir(os.path.join(temp_zip_dir, d))]
if init_at_root:
# Инит лежит в корне архива
...
elif len(subdirs) == 1:
# Инит лежит на один уровень глубже во вложенной папке
...Также на этапе тестирования пришлось побороться с кодировками: при распаковке ZIP на кириллических путях (русские буквы в именах файлов) пути превращались в кашу. Решилось это принудительным декодированием имен файлов через cp866.
Противостояние: Android, Termux и VPN
Одной из самых больших загадок при тестировании Kaguya на смартфонах (тестировал на своем Poco X6 Pro 5G) стала проблема с запуском бота‑ассистента.
На мобильном интернете с использованием VPN юзербот запускается в Termux без проблем, но на этапе привязки токена ассистента стабильно падает по таймауту. При этом обычные HTTPS‑вызовы для проверки токена проходят успешно.
Моя теория заключается в том, что агрессивные алгоритмы энергосбережения Android или особенности маршрутизации трафика через VPN в Termux не позволяют стабильно удерживать два параллельных долгоживущих соединения (MTProto‑сессию юзербота и Long Polling ассистента) в рамках одного процесса. Я пробовал отключать оптимизацию батареи для Termux, но это не помогло. К сожалению, как мы все знаем, без VPN в России Telegram запустить не получится, поэтому для мобильной версии стабильный ассистент пока остается сложной задачей. На ПК же (Windows/Linux VPS) всё работает стабильно 24/7.
Заключение
Разработка Kaguya UserBot научила меня одной важной вещи: open‑source проект — это не только чистый код и сложные алгоритмы. Это в первую очередь забота о деталях, внимание к UX обычного пользователя и готовность переписать архитектуру с нуля, если она зашла в тупик.
Проект полностью открыт, исходный код KaguyaUserBot доступен на моем GitHub. Буду рад вашим звездам, пул‑реквестам и здоровой критике в комментариях!