Cluster · разработчикам
Telegram и боты
Подключение аккаунта
На сайте открой «Подключения Telegram». Можно войти по номеру или по QR. В обоих случаях нужны собственные API ID и API hash из настроек приложения Telegram.
Для QR выбери «По QR» и нажми «Получить QR для входа». В уже открытом Telegram перейди в «Настройки» → «Устройства» → «Подключить устройство», отсканируй QR и подтверди вход. Если сайт открыт на телефоне, можно нажать «Открыть в Telegram». Истёкший QR обновляется автоматически, пока страница открыта; вся попытка действует десять минут.
Если в Telegram установлен облачный пароль, введи его на следующем шаге. Затем нажми «Завершить подключение». Смена уже подключённого аккаунта также требует пароль Cluster и, если включён, его двухэтапный вход. Вход по QR не отменяет эти проверки.
QR и ссылка для входа относятся к твоей попытке подключения. Не передавай их другим людям. В контейнер модуля они не попадают.
Методы модуля
Макет обращения к Telegram:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING: from router import Event
class Module:
@loader.command("me")
async def me(self, ctx: Event) -> None:
user = await app.client.get_me()
await ctx.reply(str(user["id"]))app.client содержит методы Telegram-аккаунта, app.bot методы личного бота, app.inline конструктор форм с кнопками. Это объекты SDK Cluster. Сам клиент Telethon, сессия и токены работают внутри службы ядра и в контейнер модуля не передаются.
Telegram-клиент
| Вызов | Аргументы | Результат | Право |
|---|---|---|---|
get_me() | Без аргументов | Объект пользователя | telegram.read |
get_messages(chat, limit=20) | chat: int, limit: int от 1 до 100 | Список сообщений | telegram.read |
send_message(chat, text, reply_to=None) | chat: int, text: str, необязательный ID сообщения | Сообщение | telegram.send |
edit_message(chat, message, text) | chat: int, message: int, text: str | Сообщение | telegram.send |
delete_messages(chat, messages) | chat: int, messages: list[int], от 1 до 100 ID | True | telegram.send |
update_profile(**data) | Строки first_name, last_name, about | Объект пользователя | telegram.profile |
update_username(username) | username: str | Результат TL в JSON | telegram.profile |
update_photo(file) | file: str, ID файла Cluster | Результат TL в JSON | telegram.profile, files.read |
send_file(chat, file, caption="", document=False, reply_to=None) | chat: int, file: str, caption: str, document: bool, необязательный ID ответа | Сообщение | telegram.send, files.read |
edit_file(chat, message, file, caption="") | chat: int, message: int, file: str, caption: str | Сообщение | telegram.send, files.read |
download_media(chat, message, maximum=10485760) | chat: int, message: int, maximum: int в байтах | Готовый файл Cluster | telegram.read, files.write |
Все вызовы в таблице выполняются через await app.client.МЕТОД(...). telegram.full тоже допускает эти операции, ограничения по чатам и пользователям остаются в силе.
Формы основных результатов можно описать так:
from typing import TypedDict
class User(TypedDict):
id: int
first_name: str
last_name: str
username: str | None
bot: bool
class Message(TypedDict):
id: int
chat: int
sender: int | None
text: str
date: intЭто описание JSON для подсказок, не классы SDK. date является Unix-временем в секундах.
Чат передаётся числовым ID, имя или ссылка вместо него не принимаются. Для пользователя ID положительный, для обычной группы отрицательный, для канала или супергруппы с префиксом -100. Используй ctx.chat_id или ID из ответа Telegram, не добавляй и не убирай префикс вручную. ID сообщения положительный, меньше 2**31. Текст отправки и редактирования до 4096 байт в UTF-8. Разметка текста не разбирается. get_messages получает последние сообщения с заданным пределом, аргумента смещения в этом методе нет.
После перезапуска ядро само получает недостающий access_hash из доступных диалогов. Это действует для отправки, редактирования, истории и файлов. Модулю не нужно хранить эти данные. Если Telegram не позволяет определить адресата, вызов возвращает invalid_input; произвольного ID незнакомого пользователя для отправки недостаточно. Найденный чат всё равно проверяется по разрешениям модуля.
delete_messages поддерживает каналы и супергруппы. Общий метод удаления только по ID сообщений без проверяемого чата не используется.
У update_profile должно быть хотя бы одно изменение, каждое значение до 256 символов. update_username принимает пустую строку для удаления либо допустимое имя: латинская буква в начале, затем латинские буквы, цифры и подчёркивание, всего от 4 до 32 символов.
Права проверяются перед ожиданием, после ожидания клиента и перед возвратом результата. Для истории и событий учитываются реальный чат и отправитель. Для отправки ограничение пользователя относится к получателю, а не собственному отправителю.
Файлы Telegram
Макет получения вложения:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING: from router import Event
class Module:
@loader.command("save")
async def save(self, ctx: Event) -> None:
file = await app.client.download_media(ctx.chat_id, int(ctx.args))
await app.db.set("last_file", file["id"])
await ctx.reply(file["filename"])Команда получает числовой ID сообщения из аргумента. Для примера нужны telegram.read, telegram.send, files.write, db.write, а также доступ к исходному чату. В рабочем модуле неверный или отсутствующий аргумент нужно обработать по своему сценарию.
download_media сохраняет документ или фотографию в раздел экземпляра. Возвращается запись файла с id, filename, size, sha256 и остальными полями файлов SDK. Путь сервера не возвращается.
maximum от 1 байта до 1 GiB, исходно 10 MiB. Проверяются фактические чат, сообщение и автор. Место резервируется до получения; части до 64 KiB. При отзыве прав, смене аккаунта, остановке, отмене или ошибке частичный файл и резерв удаляются. Для получения байтов через app.files.chunks(id) нужен ещё files.read.
Для отправки используется ID готового доступного файла Cluster, не путь сервера. send_file и edit_file допускают подпись до 1024 байт в UTF-8. document=True отправляет как документ. edit_file добавляет или заменяет вложение собственного сообщения, если Telegram разрешает такое изменение. update_photo использует тот же вид ID.
Общий запрос Telegram
await app.client.invoke(method, chat=None, **data)method: str является именем TL-метода без суффикса Request. Его параметры передаются именованными аргументами JSON. Требуется telegram.full. Для произвольного TL-запроса chat остаётся None: этот параметр не превращает произвольный запрос в безопасный вызов для конкретного чата.
Открыты пространства имён запросов установленной версии Telethon, кроме авторизации. Конструкторы доступны через app.client.types, настоящие параметры запросов через app.client.api. Примеры находятся в конструкторах Telegram. Запросы авторизации, экспорта сессии и получения служебных ключей запрещены. Точный список находится в shared/tl.py.
Типы Telethon задаются словарём с полем _, байты полем bytes с base64, дата полем date с Unix-временем. Результат тоже сериализуется в JSON, до 512 KiB.
В параметрах адресата можно передать числовой ID, например await app.client.invoke("users.GetUsers", id=[ctx.chat_id]) для личного диалога. Для каналов и супергрупп сохраняется префикс -100. Ядро получает недостающие данные так же, как в обычных методах, в том числе для списков адресатов, InputDialogPeer и InputNotifyPeer. Готовые InputUser, InputChannel и другие TL-объекты остаются допустимыми. Их поля должны соответствовать типу Telegram; вложенные TL-объекты задаются целиком. Неверные параметры отклоняются до отправки запроса с кодом invalid_input.
class Module:
async def init(self) -> None:
result = await app.client.invoke("users.GetUsers", id=[{"_": "InputUserSelf"}])
await app.db.set("self", result)Если у модуля или участника цепочки есть ограничения Telegram по чатам или пользователям, произвольный TL-запрос отклоняется. Ядро не может определить все его адресаты.
Четыре имени обрабатываются отдельно: messages.SendMessage, messages.GetHistory, channels.DeleteMessages, account.UpdateProfile. Они используют проверяемые методы из таблицы и их параметры: например await app.client.invoke("messages.GetHistory", chat, limit=20). Это не произвольные TL-параметры GetHistoryRequest. Для обычной работы удобнее сразу вызвать get_messages или другой соответствующий метод.
Ошибки Telegram
Операции аккаунта выполняются последовательно. Между началами соседних вызовов минимум 50 мс. FloodWait устанавливает общий срок ожидания для всех модулей аккаунта.
При ошибке Telegram обёртка SDK поднимает RuntimeError, первый аргумент содержит словарь ошибки:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING: from router import Event
class Module:
@loader.command("send")
async def send(self, ctx: Event) -> None:
try: await ctx.reply(ctx.args)
except RuntimeError as error:
details = error.args[0]
if isinstance(details, dict) and details.get("kind") == "flood":
await app.logs.write(f"Retry after {details['retry_after']} seconds", level="warn")
else: raiseВозможные kind: flood, authorization, forbidden, invalid, offline, temporary, telegram, unknown. У flood есть retry_after в секундах. Ошибки самого ядра, например отказ в правах, могут вместо словаря содержать строковый код.
Отправка после разрыва соединения или неизвестного серверного результата автоматически не повторяется. unknown означает, что действие уже могло выполниться. Нужно проверить результат, прежде чем повторять. Исходный текст исключения Telegram не выдаётся как публичный ответ.
Личный бот
await app.bot.get_me()
await app.bot.send_message(chat, text, **data)
await app.bot.edit_message(chat, message_id, text, **data)
await app.bot.delete_message(chat, message_id)
await app.bot.send_file(chat, file, kind="document", caption="", reply_markup=None)chat: int, text: str, message_id: int, file: str является ID файла Cluster. Дополнительные параметры отправки и редактирования принимаются по схеме Bot API. Для всех остальных методов есть app.bot.api и общий invoke. Конструкторы объектов и правила прав описаны в BUILDERS.md.
get_me возвращает объект с id, username, first_name, is_bot, требует bot.read. Отправка и редактирование возвращают данные сообщения, удаление True; нужно bot.send. Для send_file также требуется files.read.
Виды файла kind: document, photo, video, animation, audio, voice, video_note. Это же значения media.type у app.inline.form.
Для собственных кнопок в inline_keyboard поле callback_data начинается с module/slot:. Оно не создаёт подтверждённое событие @loader.callback. Формы с зарегистрированными обработчиками создаются через app.inline, их формат находится в MODULES.md.
Общий вызов:
await app.bot.invoke(method, **data)method: str, data содержит JSON-параметры Bot API. Требуется bot.full. Токен ядро подставляет само. Список разрешённых методов находится в shared/bot_methods.py. Управление адресом доставки обновлений, получение обновлений, токенов управляемых ботов и завершение службы закрыты.
Если у любого участника цепочки есть ограничения бота по чатам или пользователям, общий метод отклоняется. Тогда используются отдельные методы с проверяемым числовым ID чата.
Telegram file ID и URL в общем запросе обрабатываются по правилам Bot API. ID из app.files не является Telegram file ID. Для файла Cluster нужен app.bot.send_file.
Без личного бота Telegram и база продолжают работать. Вызов, которому нужен личный бот возвращает ошибку отсутствующего подключения.
События
Для обычной команды или сообщения используются декораторы из MODULES.md. Для самостоятельного цикла есть:
app.events(kind="telegram")kind: str принимает telegram или bot. Результат является асинхронным генератором словарей событий, не объектов Event. У события сообщения есть id, chat, sender, text, date, event, out.
class Module:
async def init(self) -> None:
async for data in app.events("telegram"):
await app.db.set("last_message", data["text"])Для примера нужны telegram.read и db.write. Telegram передаёт сообщения через обработчик ядра, личный бот через проверяемый адрес доставки обновлений. Права, источник и версия подключения проверяются перед очередью и при получении.
Очередь создаётся при первом получении, отдельная для экземпляра и источника, до 128 событий. При переполнении самое старое удаляется. Прямой events/poll возвращает {"events": [...], "dropped": ...}, до 100 событий за запрос, wait от 0 до 10 секунд. app.events выдаёт только события, для счётчика потерь используется app.request("events/poll", kind=..., wait=...).
Очередь находится в памяти. Событие не подтверждает овнерку, управление ядром через него недоступно.
Подключение аккаунта
Авторизация выполняется через сайт. Нужны номер в международном формате, собственные api_id и api_hash, действующая подписка. Первое подключение не требует повторного ввода пароля Cluster. Для замены уже привязанного аккаунта нужны пароль Cluster и код двухэтапного входа. После отправки вводится код Telegram. Если Telegram требует пароль двухэтапной защиты, ядро возвращает состояние password и ждёт его отдельно.
Попытка действует 10 минут, до пяти отправок кода или пароля. Код отправляет Telethon, пароль Telegram не сохраняется. До завершения новой авторизации прежнее подключение остаётся действующим.
StringSession, параметры API и прокси хранятся в зашифрованной служебной базе. Отдельного SQLite-файла Telethon нет. Блокировка аккаунта и владение центральной базой исключают второй клиент в другой службе Cluster.
При запуске служба восстанавливает авторизованные подключения. connected означает сохранённую авторизацию, online текущее соединение. При ошибке авторизации подключение считается отозванным (revoked). Вход на сайт сохраняется.
Смена аккаунта
Новое подключение проверяется до замены старого. Профиль Cluster, первоначальный овнер, данные и настройки сохраняются. Версия подключения увеличивается, запросы к старому аккаунту отклоняются.
Личный бот сохраняется. replace_bot: true при завершении авторизации удаляет прежнюю привязку. Подключение нового бота выполняется отдельно.
Прокси
Telegram поддерживает SOCKS4, SOCKS5 и HTTP с необязательными логином и паролем. До соединения проверяется адрес: локальные, внутренние и зарезервированные адреса запрещены. Используется проверенный IP, при восстановлении проверка повторяется.
Это прокси Telegram-аккаунта. Права вебклиента модуля задаются отдельно.
Подключение бота
Подключение, создание и отключение личного бота доступны овнерам через действующий вход на сайт, без повторного ввода пароля и одноразового кода.
Токен проверяется через getMe, один бот не назначается двум юзерботам. Сервисный бот не может стать личным. Токен остаётся в служебной базе.
Смена токена обновляет клиент. Смена самого бота также отзывает прежний секрет входящих обновлений.
Доставку обновлений настраивает оператор отдельно для сервисного и личного бота. Путь личного бота /api/bot/update/UNIT_ID, сервисного /api/bot/update. Проверяется отдельный секрет в заголовке Telegram. Модуль не получает setWebhook, getUpdates или токены.
BridgeSentinel
Сервисный бот подключается операторской командой bot/service. BridgeSentinel является отображаемым именем, Telegram username для ссылки получается из getMe.
В BotFather включается управление ботами. Пользователь подтверждает создание, ядро выдаёт ссылку Managed Bots и ждёт обновления сервисного бота. Проверяются создатель, активный запрос, срок и версия профиля. Токен получается через getManagedBotToken.
Повтор обновления не создаёт вторую привязку. Обновление токена уже подключённого управляемого бота сохраняет назначение, в том числе после смены Telegram-аккаунта юзербота. При передаче бота другому владельцу Telegram прежняя привязка отключается; профиль и данные Cluster другому человеку не передаются.
Требования к созданию описаны в документации Managed Bots.
Проверки
В автоматических тестах почта, Telegram и Bot API отвечают тестовыми данными. scripts/probe_accounts.py проверяет регистрацию через HTTP, авторизацию и вызовы из настоящего контейнера gVisor, без реальных писем и Telegram-сообщений.
Настоящий вход, доставка письма, создание бота и входящие обновления проверяются отдельно с настроенными службами. Автоматические тесты не подтверждают их доступность у конкретного оператора.
Уведомления бота
Доставка включается в общих настройках юзербота. После включения приходят только новые уведомления. Старые остаются на сайте; повторное включение не запускает рассылку накопленного. При первом запуске обновлённого ядра накопленные до обновления сообщения также остаются только на сайте. Отсечка использует порядковый номер события, поэтому новое уведомление в ту же секунду не теряется. Сообщение приходит от подключённого личного бота, при его отсутствии используется сервисный. Если отправка не удалась, ядро повторит попытку через пять минут; неуспешная отправка не помечается доставленной.
Уведомление содержит заголовок категории, текст в цитате и время события в UTC. Название модуля и другие значения перед отправкой экранируются, поэтому содержимое модуля не превращается в разметку Telegram. При настроенном публичном адресе Stripe добавляется кнопка нужного раздела сайта.
Для обновления указаны название модуля, профиль экземпляра, старая и новая версии, создание резервной копии. Установка версии и запуск различаются: сообщение об установке ещё не подтверждает, что модуль запустился. Уведомление о доступной версии объясняет, требуется ли ручное обновление или дополнительные разрешения. При отключении после ошибки указаны модуль, версия, код причины и дальнейшее действие. Полный вывод ошибки остаётся в журнале, в Telegram он не отправляется.
Язык берётся из общих настроек, поле language. В API исходный message остаётся английским; translation содержит оба перевода. Переключатели обычных уведомлений не отключают важные сообщения о безопасности, подписке, удалении и других системных событиях.