Зачем миру еще один юзербот, или Как начать всё заново после провала

Начать хочется с честного признания: Kaguya UserBot — это моя вторая попытка. Первая закончилась полным архитектурным тупиком. Система получилась настолько негибкой, что в какой‑то момент я просто забросил проект на два месяца, а затем полностью удалил код и начал всё с чистого листа.

Зачем вообще писать свой юзербот, когда вокруг полно готовых решений вроде Hikka, FTG или Dragon‑Userbot? У меня было две основные причины:

  1. Профессиональный вызов: Доказать самому себе, что я могу создать законченный, интересный и уникальный инструмент в open source.

  2. Практическая польза: Мне, как разработчику, часто нужно быстро получать 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. Но как обойти это ограничение?

Я реализовал гибридную схему, запустив в одном процессе юзербота и его бота‑ассистента. Они работают параллельно и делят общую базу данных. Когда пользователю нужно отправить сообщение с кнопками, происходит следующая магия:

  1. Пользователь пишет команду:
    .kb
    [ Привет Хабр! | https://habr.com ]
    Текст сообщения

  2. Юзербот перехватывает сообщение, парсит структуру кнопок и текст.

  3. Поскольку юзербот и бот‑ассистент живут в разных областях видимости API, передать данные напрямую сложно. Поэтому юзербот генерирует случайный UUID и сохраняет структуру сообщения в кэш diskcache.

  4. Юзербот мгновенно удаляет исходное сообщение.

  5. Юзербот инициирует скрытый инлайн‑запрос (Inline Query) к своему боту‑ассистенту, передавая UUID: kb_<UUID>.

  6. Бот‑ассистент принимает инлайн‑запрос, считывает данные из общего diskcache, формирует InlineQueryResult со встроенной клавиатурой и отдает его юзерботу.

  7. Юзербот отправляет этот результат в чат от имени пользователя.

Демонстрация инлайн-плагина
Демонстрация инлайн‑плагина

На выходе мы получаем бесшовный опыт: вы написали команду, она исчезла, и вместо нее появилось красивое сообщение с кнопками, отправленное лично вами. Но такой метод не поддерживает премиум эмодзи на уровне Telegram API.

Психология ошибок, или Почему Microsoft Word — это новая Figma

Изначально я надеялся, что все пользователи будут строго следовать текстовой инструкции по созданию и привязке бота‑ассистента. Но золотое правило разработки гласит: «Если разработчик где‑то позабыл — пользователь гарантированно забудет сделать нужное действие со 100% вероятностью».

Самая частая проблема: пользователь привязывает токен бота через команду .token, но забывает включить в настройках @BotFather инлайн‑режим. Юзербот перехватывает ошибку BotInlineDisabled, но как сообщить об этом пользователю?

Красный текст об ошибке вызывает тревогу и отторжение. Я решил сделать дружелюбный визуальный баннер‑инструкцию:

  • Пытался сгенерировать его через ИИ — выходило красиво, но с кривыми буквами и артефактами в интерфейсе Telegram.

  • Фотошоп показался мне слишком неудобным для быстрой верстки.

  • В итоге я взял инструмент, в котором за свою жизнь оформил сотни отчетов и таблиц — Microsoft Word! Простые геометрические фигуры, скриншоты, наложение неонового свечения на стрелочки — и баннер готов.

Баннер сделанный в Word
Баннер сделанный в 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. Буду рад вашим звездам, пул‑реквестам и здоровой критике в комментариях!