Cluster · разработчикам

Cluster: юзербот для Telegram

Cluster («Кластер») предоставляет юзербота для Telegram, функционал которого собирается из модулей. Профиль, подключения, права и установленные модули настраиваются на сайте. Telegram Mini App открывает тот же интерфейс.

Сам юзербот работает на сервере. Браузер можно закрыть, модули продолжат работу. Для входа используются почта, логин и пароль Cluster. Telegram подключается отдельно, его можно заменить без создания нового профиля.

Функционал

Модули могут обрабатывать сообщения, команды, кнопки, запросы сайта и входящие HTTP-запросы. Для них доступны Telegram, личный бот, база данных, файлы, сеть, расписание и обращение к другим модулям. Каждая возможность требует отдельного права.

На сайте есть обзор юзербота, список модулей, вебшоп, конфигуратор, подключение Telegram, терминал, журнал, уведомления и резервные копии. Один профиль может управлять своим юзерботом и другими, в которых ему выдали овнерку.

Регистрация открыта. Для работы юзербота нужна подписка или подарочный код. Оплата проходит через Stripe: 20 долларов за 30 дней, 60 за 90 дней и 200 за 365 дней. Подписка продлевается автоматически, следующие списания можно отключить в панели. Продажа модулей и произвольные HTML-страницы модулей пока не реализованы.

Устройство

Начнём с трёх составляющих: профиль, юзербот и экземпляр модуля.

Профиль хранит данные входа. Юзербот содержит Telegram-подключение, личного бота, настройки и хранилище. Экземпляр содержит установленную версию модуля, принятые права и собственные данные. Один модуль можно установить несколько раз, выбрав разные имена экземпляров.

Сайт написан на React и TypeScript. FastAPI принимает запрос, проверяет его формат и передаёт ядру через закрытый Unix-сокет. Ядро определяет действующий профиль и проверяет право на операцию. Только ядро открывает базы Mekansm и хранит настоящие Telegram-сессии.

Модуль выполняется в отдельной среде gVisor. В ней нет прямой сети, файла сессии или служебной базы. Вместо них модуль получает клиентский App: он передаёт запрос ядру и возвращает результат. Для сборки зависимостей используется отдельная изолированная среда.

Браузер / Telegram Mini App
             |
          FastAPI
             |
       закрытый сокет
             |
            Ядро
       /      |       \
 Telegram   Mekansm   Проверка прав
                         |
                   Среда модуля
                   клиентский App

Стрелка к среде показывает доставку задания. Сам модуль также обращается обратно к ядру через App. Реальный объект Telethon, токен бота и файл базы в среду не передаются.

Права

Права указываются в описании версии. При установке пользователь выбирает, какие из них принять. Не указал право или не принял его, соответствующая операция недоступна.

Например, модуль получает и сохраняет данные в своём разделе базы, а сообщения отправляет в один чат:

{
    "db.read": {},
    "db.write": {},
    "telegram.send": {
        "chats": [-1001234567890]
    }
}

Пустой объект у конкретного права означает, что дополнительных ограничений нет. Пустой объект всех прав {} не разрешает ничего. Пользователь может сузить права, но не расширить их сверх описания автора.

Отдельно проверяются полномочия человека. Овнер управляет юзерботом, доверка получает выбранные действия. Доступ к одной команде не даёт терминал или управление аккаунтом. Настройки отдельного модуля могут перекрыть общие настройки юзербота в своём разделе.

Полные разрешения тоже проходят через ядро. telegram.full открывает поддержанные низкоуровневые методы, но не настоящую сессию. При ограничении по чатам запрос, адресатов которого нельзя проверить, отклоняется.

Макет модуля

Основной файл пакета называется main.py, в нём объявляется класс Module. При загрузке доступны app и loader. После создания класса в глобальную область добавляется module, экземпляр этого класса.

Обычный init используется для подготовки модуля. Команды, кнопки и другие обработчики отмечаются декораторами. Пример команды со своим счётчиком:

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING: from router import Event

class Module:
    @loader.command("count", source="telegram")
    async def count(self, ctx: Event) -> None:
        value: int = await app.db.get("count", default=0)
        value += 1
        await app.db.set("count", value)
        await ctx.reply(str(value))

ctx является событием Cluster. В нём находятся текст, аргументы команды, ID чата, отправитель и данные действия. ctx.reply отвечает в исходный чат с проверкой действующих прав.

Вызов app.db.get получает значение из раздела своего экземпляра, set сохраняет новое. Для этого примера нужны db.read, db.write, telegram.read и telegram.send. Префикс команды задаётся в ядре, изначально !.

Описание пакета отдельно перечисляет обработчик и права. При запуске ядро сравнивает описание с кодом. Скрытый дополнительный обработчик или другой уровень доступа не принимаются. Аннотация Event нужна для удобства разработки, принадлежность события проверяется ядром при выполнении.

Команда

Посмотрим, что происходит после !count.

Telegram-подключение ядра получает сообщение. Диспетчер находит алиас, выбирает экземпляр и проверяет человека, чат, подписку и права модуля. Затем создаёт событие с временным ключом вызова и передаёт его обработчику.

Обработчик обращается к базе через app.db. Ядро повторно проверяет подключение экземпляра и контекст события, получает данные и возвращает результат. При отправке ответа проходит ещё одна проверка Telegram-прав. Успех первой проверки не даёт модулю постоянный пропуск на все следующие операции.

Обработчики одного экземпляра выполняются последовательно. Необработанная ошибка или превышение срока отключает экземпляр с ошибкой. Он ждёт ручного включения или переустановки. Ошибку можно обработать внутри модуля, если автор предусмотрел такой случай.

Кнопки и конфигуратор

Кнопка создаётся через app.inline.form, обработчик отмечается @loader.callback. В Telegram передаётся случайное значение ядра, связанное с конкретными экземпляром, ботом, чатом и сообщением. Его нельзя перенести в другое сообщение и получить тот же вызов.

Конфигуратор строится из Module.config. Автор описывает поле, тип, подпись, начальное значение и ограничения. Поддержаны строки, секреты, числа, переключатели, выбор, списки, JSON и файлы. Значение секрета сайт обратно не получает, только отметку о его наличии.

Страница модуля собирается из текста, показателей, таблиц, кнопок и форм. Автор задаёт элементы, ядро проверяет значения, React отображает результат. Произвольный JavaScript из модуля на сайте не выполняется.

Вебшоп

Пакет загружается на сайт архивом, по HTTPS-ссылке или из GitHub. Установки файлом через Telegram-команду нет. Первый пакет получает постоянный ID публикации, следующие версии используют тот же ID.

Версия содержит описание, права, зависимости, историю изменений и закреплённый пакет. Опубликованное содержимое не меняется. Исправление оформляется новой версией, GitHub-пакет привязывается к конкретному коммиту.

Публикация не требует ручной проверки. Автор может запросить её отдельно. Галочка относится к проверенной версии и означает соответствие функционала описанию по результату проверки. Следующая версия её не наследует.

Python-зависимости задаются в описании модуля. При сборке они получаются из PyPI; также можно приложить готовые пакеты. Прямой сети у среды сборки нет, запросы к PyPI передаются через ядро с проверкой адресов и размера. Сторонний код и установочные скрипты выполняются в изоляции, а не в процессе ядра.

Обновления

Есть три режима: уведомление о новой версии, автоматическое обновление без новых прав и автоматическое с принятием новых прав. Последний выбирается явно. Перед обновлением видны добавленные и убранные права.

По умолчанию создаётся одна копия данных экземпляра. Она входит в пользовательскую квоту и заменяет предыдущую. Если обновление завершилось ошибкой, экземпляр отключается. Данные автоматически назад не откатываются, пользователь выбирает версию и при необходимости восстанавливает копию.

Данные и обмен

У каждого юзербота своя база Mekansm. У экземпляра свой раздел. Файлы, журнал, задачи, сборки и копии учитываются в общей квоте юзербота. При 90% появляется предупреждение, при заполнении запрещается увеличение данных.

Для обмена между юзерботами создаётся договор. В нём указаны участники, экземпляры, ресурс, формат и направление. Адресат одобряет запрос, после чего наличие ресурса проверяется фактически. Договор разрешает обмен данными, выполнение чужих команд он не открывает.

Отзыв запрещает новые передачи. Уже принятая передача может закончиться. Если получатель выключен, отправитель сразу получает недоступность, скрытой очереди до следующего запуска нет.

Терминал и копии

Овнерам доступен Bash через сайт и !exec. Он работает в собственной среде gVisor, имеет постоянные файлы, ввод и остановку процессов. Доверка и модули не могут вызвать его API. Фактическая Telegram-сессия и сервер управления в терминал не передаются.

Полная копия сохраняет прикладные данные, настройки, файлы, задачи, журнал и рабочие файлы терминала. Сессии Telegram и входы сайта в неё не входят. Секреты, которые пользователь сохранил в конфигурации модуля, могут входить в архив, поэтому скачанную копию нужно хранить соответственно.

Для восстановления юзербот останавливается. Ядро проверяет состав и суммы файлов, готовит их отдельно и сохраняет состояние переключения. После сбоя оно завершает это переключение при запуске. Модули остаются остановленными до ручного включения.