Cluster · разработчикам
Конструкторы сообщений
Сообщение, форма, клавиатура, кнопка, вложение, альбом и страницы представлены отдельными объектами Cluster. Их можно собрать до отправки, сохранить в переменной и менять после отправки. Кнопки находятся по постоянному ключу, поэтому перестановка ряда не меняет их назначение.
Объекты работают в процессе модуля. Ядро отправляет запросы в Telegram, хранит привязки форм и проверяет права. Клиент Telethon, токен бота и файл сессии модуль не получает.
Записи, файлы, сеть, расписание, страницы сайта и остальные разделы описаны в объектах SDK.
Объекты
| Создание | Объект | Назначение |
|---|---|---|
app.message(ctx, text="...", source=None) | Message | Сообщение аккаунта или личного бота |
app.inline(ctx) | Form | Форма с обработчиками кнопок |
app.inline.button(text, callback=None, key=None, **data) | Button | Кнопка с постоянным ключом |
app.inline.keyboard(rows=None) | Keyboard | Ряды и порядок кнопок |
app.inline.pages(items, size=5, index=0) | Pages | Разбиение списка и кнопки перехода |
app.media(file, kind="document", caption="", filename=None) | Media | Файл Cluster или байты для загрузки |
app.album(ctx, source=None) | Album | Набор вложений с подписями |
app.text(value="") | Text | Текст с безопасным оформлением |
Вместо ctx можно передать числовой ID чата. У сообщения и альбома источник берётся из события; без события используется Telegram-аккаунт. source="bot" выбирает личного бота, source="telegram" аккаунт. Сборка объекта не отправляет запросы. Для отправки вызывается await.
Форма
Полный пример с типами:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING: from router import Event
class Module:
@loader.command("panel", source="bot", access="owner")
async def panel(self, ctx: Event) -> None:
form = app.inline(ctx)
keyboard = app.inline.keyboard([
[app.inline.button("Обновить", self.refresh, key="refresh")],
[app.inline.button("Документация", key="docs", url="https://cluster.ninepeaks.dev/developers/")],
])
await form.form(text="Выбери действие", keyboard=keyboard)
@loader.callback(access="owner")
async def refresh(self, ctx: Event) -> None:
await ctx.form.rename_button("refresh", "Обновить ещё раз")
await ctx.answer("Готово")Для примера в описании пакета нужны команда panel и обработчик refresh, а из прав bot.read и bot.send. @loader.callback берёт ID из имени метода. Строка с именем не передаётся первым аргументом декоратора. Для одного экземпляра формы доступны оба вида вызова:
form = await app.inline(ctx).form(text="Настройки", keyboard=keyboard)
form = await app.inline(ctx).form({"text": "Настройки", "keyboard": keyboard})form.form(...) отправляет объект один раз и возвращает его. form.id содержит ID формы Cluster, form.message_id ID сообщения Telegram, form.message данные отправки. form.text, form.media, form.keyboard содержат текущее содержимое.
Кнопки и клавиатура
У каждой кнопки есть key. Если он не задан, создаётся случайный ключ. Для кнопок, которые нужно менять из обработчика или после перезапуска модуля, задавай ключ явно.
button = app.inline.button("Обновить", self.refresh, key="refresh")
button.rename("Ещё раз").disable()
button.enable().hide()
button.show()
keyboard = app.inline.keyboard([[button]])
keyboard.add(app.inline.button("Закрыть", self.close, key="close"), row=0)
keyboard.row(app.inline.button("Сайт", key="site", url="https://cluster.ninepeaks.dev/"))
keyboard["site"].move(0, 1)
keyboard.sort(reverse=True)
keyboard.layout(2, 1)
keyboard.remove("close")row(*buttons) добавляет новый ряд, add(*buttons, row=None, index=None) вставляет кнопки в существующий или следующий ряд. Без row используется последний ряд. move(key, row, index=0) переносит кнопку. Пустые ряды удаляются. Одна кнопка не может одновременно находиться в двух клавиатурах; её можно сначала удалить и добавить в другую.
sort(key=None, reverse=False, per_row=False) сортирует всю клавиатуру с сохранением размеров рядов. Без key сравнивается текст без учёта регистра. per_row=True сортирует каждый ряд отдельно. Для вычисляемых подписей задай свою функцию key, например по постоянному ключу кнопки. layout(2, 1) задаёт два элемента в первом ряду, по одному в последующих; последнее число повторяется. clear() удаляет все кнопки.
button.remove() удаляет кнопку из её клавиатуры. hide() сохраняет описание, но не отправляет кнопку в Telegram. disable() показывает неактивную кнопку. После show() или enable() её можно вернуть. Удаление, скрытие и отключение отзывают прежние привязки нажатий при следующем успешном обновлении формы.
Для нескольких локальных изменений достаточно одного запроса:
form.button("refresh").rename("Обновить данные")
form.button("site").hide()
form.keyboard.sort(key=lambda button: button.key)
await form.edit(text="Данные обновлены")Если нужно сразу применить одно изменение, у формы есть rename_button, drop_button, hide_button, show_button, disable_button, enable_button, move_button и sort_buttons. Эти методы вызываются через await и возвращают ту же форму. Методы самой кнопки и клавиатуры меняют только объект; отправка выполняется через await form.update() или await form.edit(...).
Обработчики и вычисляемые подписи
В callback можно передать строковый ID или связанный метод с @loader.callback. Для связанного метода конструктор сам получает ID. Произвольная функция без декоратора обработчиком не становится.
button = app.inline.button("Следующая", self.page, key="next", values={"page": 2})
button = app.inline.button("Следующая", self.page, key="next", args=(2,))
button = app.inline.button(lambda form: str(form.state.get("count", 0)), self.refresh, key="count")values содержит именованные поля обработчика. args допускается со связанным методом: словарь дополняет values, список или кортеж сопоставляется полям в порядке их объявления. Количество аргументов должно совпасть. Поля всё равно проверяются ядром по описанию обработчика.
Подпись может быть обычной строкой, функцией без аргументов или функцией, принимающей форму. Допускается асинхронная функция. Она вызывается перед отправкой и обновлением, а результат должен быть непустой строкой. form.state является локальным словарём для таких данных. Его содержимое и Python-функции не сохраняются после перезапуска; постоянные данные храни через app.db.
Нажатие приходит в обычный @loader.callback. В ctx.form находится связанная форма, в ctx.button_id постоянный ключ нажатой кнопки, в ctx.values аргументы. Права и подтверждение определяются описанием обработчика и настройками ядра.
Изменение и восстановление
await form.edit(text="Новый текст")
await form.edit(media=app.media(file_id, "photo"), text="Новая подпись")
await form.edit(keyboard=another_keyboard)
await form.update()
await form.close()Ядро выбирает редактирование текста, подписи или вложения. При изменении клавиатуры создаются новые привязки, старые отзываются. Ошибка Telegram оставляет в ядре прежнее описание формы. Локальные правки объекта сохраняются, их можно исправить и повторить. Запрос с сетевой ошибкой может быть принят Telegram до обрыва связи; автоматической повторной отправки нет.
Вложение можно заменить на поддерживаемый Telegram тип. Превратить сообщение с вложением обратно в обычный текст одним редактированием Telegram не позволяет; создай другую форму. Для альбомов и отдельных типов файлов действуют ограничения Telegram.
ID формы можно сохранить через app.db. После перезапуска:
form = app.inline.get(form_id)
await form.load()
form.button("refresh").rename("Обновить")
await form.update()load() получает текст, файл, клавиатуру и параметры оформления из ядра. Восстановленные подписи уже являются строками; вычисляемую подпись можно назначить заново через rename. Асинхронные методы формы сами загружают состояние, если объект ещё пуст. Для прямого доступа к form.button(...) после восстановления сначала нужен load().
Срок жизни ttl от 1 до 604800 секунд, исходно 3600. Изменение формы его не продлевает. До 16 рядов, до 8 кнопок в ряду. users ограничивает нажатия списком до 100 Telegram ID, но не скрывает сообщение. Истёкшая форма, чужой экземпляр, другая версия модуля, заменённый бот или отозванные права отклоняются.
Отправка через аккаунт
Обычная форма отправляется личным ботом. Для отправки через Telegram-аккаунт укажи via="inline":
await app.inline(ctx).form(text="Панель", keyboard=keyboard, via="inline")Аккаунт запрашивает подготовленный результат у личного бота и отправляет в выбранный чат. Боту не нужно состоять в этом чате. Требуются telegram.send и bot.send для адресата. В BotFather включи /setinline, а для редактирования /setinlinefeedback со значением 100%. У ядра уже предусмотрено получение этих обновлений. Без inline-режима отправка завершается ошибкой до создания сообщения.
Inline-запрос содержит случайный ключ. Получить результат может только подключённый аккаунт; чужой запрос получает пустой список. inline_message_id принимается из подтверждённого уведомления Telegram и связывается с формой. Если уведомление не пришло, форма остаётся отправленной, но редактирование сообщает об отсутствии привязки. Telegram не гарантирует доставку этих уведомлений, поэтому повторная отправка вместо редактирования не выполняется.
Для inline-вложения ядро получает Telegram file ID через временную загрузку личным ботом в диалог с владельцем и удаляет временное сообщение. Для этого дополнительно нужны files.read, право bot.send на этот личный диалог и начатый диалог с ботом. Поддержаны фото, видео, анимация, аудио, документ и голосовое сообщение. Видеокружок как inline-результат Telegram не поддерживает. Личная форма с receiver отправляется только напрямую ботом, без via="inline".
Сообщения
message = app.message(ctx, text=app.text().bold("Cluster").line("Работает"))
await message.send()
message.set_text("Новый текст")
await message.update()
message.attach(file_id, "photo")
await message.edit("Подпись к фото")
await message.close()set_text(...) и attach(file, kind="document", **data) меняют объект. send(chat=None, **options) отправляет, edit(text=None, **options) применяет содержимое, update() применяет уже сделанные правки. У объекта доступны id, chat_id, result, source, text, media и keyboard.
Если клавиатура задана до отправки, сообщение создаёт форму. Для Telegram-аккаунта используется inline-режим, для бота обычная отправка. Дальше кнопки меняются так же, как у формы:
message = app.message(ctx, text="Действия", source="bot")
message.keyboard.add(app.inline.button("Обновить", self.refresh, key="refresh"))
await message.send()
message.keyboard["refresh"].rename("Ещё раз")
await message.update()Чтобы добавить зарегистрированные кнопки к уже отправленному обычному сообщению, используй await message.replace(). Сначала отправляется новая форма, затем удаляется прежнее сообщение. При отказе отправки старое сообщение остаётся. Если отправка удалась, а удаление не удалось, новый объект остаётся доступен и ошибка возвращается вызывающему; в чате могут остаться оба сообщения.
close() удаляет собственное сообщение. В обычных диалогах аккаунта ядро сначала сверяет реального автора и чат, затем удаляет только указанное сообщение. Для чужого сообщения этот метод недоступен. Права Telegram в каналах могут дополнительно ограничить удаление.
Текст
text = app.text().bold("Заголовок").line("Обычный текст")
text.line().quote(app.text().italic("Пояснение"))
text.line().link("Открыть сайт", "https://cluster.ninepeaks.dev/")
text.line().code("print('<example>')")
await app.message(ctx, text=text).send()add, line, bold, italic, underline, strike, spoiler, code, pre, quote, link возвращают тот же объект. Текстовые значения экранируются. Вложенный объект Text сохраняет оформление. str(text) даёт HTML, text.plain обычный текст, text.length длину в единицах UTF-16. clear() очищает содержимое.
Message, Form и Album сами выбирают HTML, когда получают Text. Если передать str(text) вручную в короткий метод SDK, укажи parse_mode самостоятельно. Лимиты текста и допустимые сочетания оформления определяет Telegram.
Вложения и альбомы
photo = app.media(file_id, "photo", caption="Подпись")
document = app.media(b"Example", "document", filename="example.txt")
await app.message(ctx, media=document).send()
album = app.album(ctx).add(photo).add(another_file_id, "photo", caption="Вторая подпись")
album.sort().move(0, 1)
await album.send()
await album.message(0).edit("Новая подпись")
await album.close()Media принимает ID файла Cluster или байты с обязательным filename. Типы: photo, video, animation (также gif), audio, document, voice, video_note. Байты загружаются в хранилище при отправке, повторное использование объекта не создаёт ещё одну копию файла. Нужны files.write и files.read; для готового ID достаточно files.read. Файлы занимают место в накопителе модуля и удаляются через app.files.delete или по заданному сроку.
В альбоме от 2 до 10 элементов. Фото можно сочетать с видео, аудио только с аудио, документы только с документами. add(...), remove(index), move(index, target) и sort(key=None, reverse=False) меняют подготовленный набор. Без key сортируются подписи без учёта регистра. send(...) отправляет альбом один раз. Переставить вложения уже отправленного альбома Telegram не позволяет; методы изменения порядка после отправки отклоняются. Для другого порядка создай новый альбом. После отправки album.messages содержит результаты, album.message(index) даёт объект соответствующего сообщения для изменения или удаления. close() последовательно удаляет сообщения; если одно удаление отклонено, предыдущие уже могли выполниться.
Для альбома аккаунта нужны telegram.send и files.read, для бота bot.send и files.read. На аккаунте общие параметры отправки применяются ко всему альбому; подписи задаются отдельно. На стороне бота параметры вложения берутся из каждого Media.options. Параметры разных транспортов не взаимозаменяемы.
Страницы
pages = app.inline.pages(items, size=5, index=0)
keyboard = pages.navigation(self.page, values={"section": "history"})
await app.inline(ctx).form(text="\n".join(map(str, pages.current)), keyboard=keyboard)current содержит элементы текущей страницы, count число страниц (минимум одну), index номер с нуля. go(index) проверяет номер, next() и previous() останавливаются на краях. navigation(callback, values=None, field="page") создаёт клавиатуру перехода. Номер добавляется к аргументам обработчика под именем field. Сам список не передаётся в callback и не сохраняется автоматически.
Обработчик перехода получает номер из ctx.values, заново получает данные и строит Pages с нужным index, затем обновляет ctx.form. Поле номера и остальные values должны присутствовать в описании обработчика. Одна неактивная кнопка показывает положение в списке.
Пример модуля
Исходник примера и описание пакета содержат команду счётчика, вычисляемую подпись, аргументы обработчика, скрытие ссылки и закрытие формы. Зависимости из PyPI для этих конструкторов не нужны. Порядок публикации описан в STORE.md.
Личный ответ внутри группы сохраняется и при app.message(ctx, ...): получатель берётся из исходного события. Отправка личного альбома внутри группы не поддерживается Telegram, поэтому конструктор возвращает ошибку без отправки в общий чат.
Дополнительные методы Telegram
Для редких типов сообщений и операций остаются конструкторы настоящих полей Bot API и Telethon. Они описаны ниже. Это дополнительный уровень: обычные формы, кнопки и сообщения собираются объектами Cluster.
Bot API
Объекты создаются через app.bot.types:
class Module:
async def init(self) -> None:
button = app.bot.types.InlineKeyboardButton(text="Документация", url="https://cluster.ninepeaks.dev/developers/")
keyboard = app.bot.types.InlineKeyboardMarkup(inline_keyboard=[[button]])
await app.bot.api.sendMessage(chat_id=101, text="Описание модуля", reply_markup=keyboard, disable_notification=True)Результат конструктора является обычным словарём. Его можно сохранить, дополнить и вложить в другой объект. Неизвестное поле или отсутствие обязательного аргумента вызывает ValueError до запроса к ядру. У типов с постоянным полем type оно подставляется автоматически, например InputMediaPhoto(media=...) получает type="photo".
Справочник содержит все методы и типы текущего Bot API, включая вложенные клавиатуры, опросы, сообщения с составным оформлением и личные сообщения внутри групп. Параметры приведены в документации Telegram. Копия перечня полей находится в bot_schema.py.
Имена можно получить в самом модуле:
class Module:
async def init(self) -> None:
names = dir(app.bot.api)
fields = app.bot.types.describe("EphemeralMessageParameters")
await app.logs.write(str({"methods": names, "fields": fields}))describe возвращает словарь полей с type, required и, где есть, constant. Это описание аргументов, не результат запроса к Telegram. Условия сочетания полей, размеры медиа и доступность действия проверяет сам Telegram.
Права Bot API
Обычные методы отправки, редактирования, удаления и работы с личными сообщениями используют bot.send. getMe требует bot.read. Проверяются чат и получатель, если он указан. Для отправки файла Cluster дополнительно требуется files.read.
Остальные методы требуют bot.full. Это относится к управлению группой, опросам, пересылке и другим общим запросам. Полный доступ также нужен для business_connection_id, allow_paid_broadcast и suggested_post_parameters. Параметр оплаты может расходовать баланс бота; не включай его в обычную отправку без необходимости.
Если у экземпляра или участника цепочки вызова есть ограничения бота по чатам и пользователям, общий запрос отклоняется. Наличие bot.full не снимает эти ограничения.
Служебные методы не передаются модулю: получение обновлений, смена адреса их доставки, выход из службы, получение и замена токенов управляемых ботов и смена их служебного доступа. Они нужны самому Cluster. Точный перечень находится в bot_api.py.
Файлы в общих запросах
Для Telegram file ID достаточно строки. Для файла из хранилища Cluster используй app.bot.types.file(id):
class Module:
async def init(self) -> None:
file = await app.files.upload("example.txt", b"Cluster example")
document = app.bot.types.file(file["id"])
await app.bot.api.sendDocument(chat_id=101, document=document, caption="Пример файла")В этом примере нужны files.write, files.read и bot.full. Конструктор файла возвращает ссылку {"$file": "ID"}. Ядро проверяет доступ к файлу, открывает его и отправляет через multipart. Путь сервера модулю не возвращается.
Такие ссылки можно вкладывать в media, обложки и другие объекты, в том числе в альбом. На один запрос допускается до 16 файлов. Путь вроде /etc/passwd не является ID файла Cluster и не принимается.
Для обычной отправки одного собственного файла достаточно app.bot.send_file с bot.send и files.read. Дополнительные параметры, например parse_mode, has_spoiler и disable_notification, передаются именованными аргументами. Набор параметров должен соответствовать выбранному виду файла.
Совместимость со словарями
app.inline.form связывает кнопку с обработчиком модуля. Кроме зарегистрированных обработчиков, можно добавлять остальные виды кнопок Bot API: ссылки, копирование текста, переход к выбору встроенного запроса, Mini App и другие. Их доступность в выбранном чате определяет Telegram.
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING: from router import Event
class Module:
@loader.command("panel", source="bot", access="owner")
async def panel(self, ctx: Event) -> None:
form = await app.inline.form(ctx.chat_id, "Выбери действие", [
[{"text": "Обновить", "callback": "refresh", "style": "primary"}],
[app.bot.types.InlineKeyboardButton(text="Документация", url="https://cluster.ninepeaks.dev/developers/")],
], disable_notification=True)
await app.db.set("panel", form["form"])
@loader.callback(access="owner")
async def refresh(self, ctx: Event) -> None:
await ctx.edit("Обновлено", parse_mode="HTML")Для примера нужно объявить команду и обработчик в interface, принять bot.read, bot.send и db.write. Описание обработчиков приведено в MODULES.md.
У кнопки обработчика сохраняются callback, values и once. Дополнительно доступны style, icon_custom_emoji_id и disabled. У остальных кнопок используются настоящие поля Bot API. Нельзя одновременно указать несколько действий или подставить чужой callback_data вместо зарегистрированного обработчика.
По умолчанию форма отправляется методом sendMessage. Для другого способа укажи method, например sendPoll или sendRichMessage, и его аргументы. Метод должен создавать сообщение и поддерживать reply_markup. Параметры, которых нет в методе Telegram, отклоняются. chat_id, клавиатуру и получателя задаёт ядро по аргументам формы.
В media по-прежнему можно передать {"file": "ID", "type": "photo"} для собственного файла Cluster. Если используется другой способ отправки медиа, передай настоящий параметр метода и ссылку app.bot.types.file(id).
Личная форма внутри группы
Аргумент receiver задаёт Telegram ID получателя. Другие участники группы не увидят такую форму. Это не то же самое, что users: список users ограничивает нажатия, но сам по себе не скрывает обычное сообщение.
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING: from router import Event
class Module:
@loader.callback(access="owner")
async def private(self, ctx: Event) -> None:
await app.inline.form(ctx.chat_id, "Личные настройки", [
[{"text": "Обновить", "callback": "refresh"}],
], receiver=ctx.sender_id, replace=True)
@loader.callback(access="owner")
async def refresh(self, ctx: Event) -> None:
await ctx.edit("Настройки обновлены")replace=True показывает личную форму на месте сообщения с нажатой кнопкой только у этого пользователя. Аргумент допустим внутри действующего обработчика кнопки. Ядро само подставляет ID нажатия и сверяет отправителя с получателем.
Без прав администратора бот должен ответить на подходящее действие пользователя в течение 15 секунд. Администратор группы может отправить личное сообщение участнику без такого действия. Доставка не гарантируется, особенно если пользователь не в сети. Сообщение может исчезнуть со временем или после перезапуска приложения. Это условия Telegram, срок ttl формы их не продлевает.
При получении скрытой команды в ctx.data и ctx.ephemeral_message_id сохраняется её идентификатор. ctx.reply автоматически отвечает лично отправителю. Обычное сообщение в общий чат вместо личного при ошибке не отправляется.
Изменение формы по ID
Успешное создание возвращает поле form. Это ID формы в пределах экземпляра. Его можно сохранить в данных модуля и использовать вне обработчика кнопки:
class Module:
async def update_panel(self, form: str) -> None:
await app.inline.edit(form, "Новое содержимое", parse_mode="HTML")
async def remove_panel(self, form: str) -> None:
await app.inline.delete(form)Для обычной формы ядро вызывает обычные методы редактирования и удаления, для личной соответствующие editEphemeralMessage... и deleteEphemeralMessage. Получатель и ID сообщения берутся из сохранённой формы. Подменить их в дополнительных параметрах нельзя. Клавиатура сохраняется при редактировании.
У app.inline.edit можно выбрать method для другого способа редактирования, например editMessageCaption. Поля метода передаются именованными аргументами. Сложные операции, для которых нет проверки области доступа, требуют bot.full. Истёкшая форма, заменённый бот и форма из другой версии модуля не принимаются.
Telethon
Для запросов аккаунта используются app.client.types и app.client.api. Объекты создаются теми же именами, которые есть в telethon.types, запросы именами из telethon.functions, без окончания Request.
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING: from router import Event
class Module:
@loader.command("react", source="telegram", access="owner")
async def react(self, ctx: Event) -> None:
reaction = app.client.types.ReactionEmoji(emoticon="👍")
await app.client.api.messages.SendReaction(peer=ctx.chat_id, msg_id=ctx.id, reaction=[reaction])Нужен telegram.full без ограничений, которые нельзя проверить в общем запросе. Числовые ID пользователей и чатов разрешает ядро. Для супергрупп сохраняется префикс -100.
Это настоящий запрос messages.SendReaction, а не переименование одного из коротких методов SDK. Например, app.client.api.messages.SendMessage принимает peer, message и остальные поля Telethon. Старый app.client.invoke("messages.SendMessage", chat, text=...) сохранён для совместимости и использует прежние аргументы SDK.
Доступны все конструкторы установленной версии Telethon, включая вложенные типы с пространством имён. Для подписи аргументов используй describe:
class Module:
async def init(self) -> None:
fields = app.client.api.messages.describe("SendReaction")
constructors = dir(app.client.types)
await app.logs.write(str({"fields": fields, "types": constructors}))Байты в конструкторах автоматически передаются как {"bytes": "BASE64"}, даты как {"date": UNIX_TIME}. У datetime должен быть указан часовой пояс. Вложенные списки и объекты преобразуются рекурсивно. На стороне ядра глубина TL-объектов ограничена 24 уровнями.
Справочник запросов и конструкторов находится в tl_schema.py, исходный API в документации Telethon. При обновлении библиотеки перечень пересобирается из её классов, имена не угадываются.
Параметры обычных методов Telethon
Для обычной отправки необязательно собирать низкоуровневый запрос. app.client.send_message, edit_message, send_file и edit_file принимают дополнительные именованные параметры соответствующих методов Telethon: форматирование, клавиатуры, уведомления, расписание и параметры медиа.
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING: from router import Event
class Module:
@loader.command("format", source="telegram", access="owner")
async def format(self, ctx: Event) -> None:
entity = app.client.types.MessageEntityBold(offset=0, length=7)
await app.client.send_message(ctx.chat_id, "Cluster работает", formatting_entities=[entity], link_preview=False, silent=True)Для примера достаточно telegram.read и telegram.send в нужном чате. Без parse_mode текст остаётся обычным. Для разбора разметки укажи parse_mode="html" или parse_mode="md". formatting_entities принимает объекты, собранные через app.client.types.
send_as и comment_to затрагивают дополнительные адресаты, поэтому требуют telegram.full без ограничений. В file и thumb передаются ID файлов Cluster, а не пути сервера. Выполняемый Python, progress_callback и объект клиента из контейнера через JSON не передаются. Для операций загрузки и скачивания используй файловые методы SDK, для остальных возможностей Telegram настоящие TL-запросы.
Служебные ограничения Telethon
Набор конструкторов полный, но не каждый запрос можно выполнить от имени модуля. Авторизация, экспорт сессии, получение временных паролей и управление служебными ключами закрыты. Общие обёртки вроде InvokeWithLayer тоже не выполняются: иначе через них можно было бы вложить запрещённый запрос.
Все остальные пространства имён установленной версии доступны через общий вызов с telegram.full. Полный доступ не отменяет ограничений Telegram по типу аккаунта, правам в чате и частоте запросов.
Поддержка метода означает, что SDK умеет собрать и передать его в ядро. Успешное выполнение зависит от параметров, прав аккаунта и ответа Telegram. Автоматические проверки используют отдельную среду с заменённым Telegram; они не рассылают сообщения и не выполняют платежи настоящих пользователей.