Перейти к содержимому

UI-компоненты

UI-компоненты — это способ организовать сложных ботов в изолированные «экраны», каждый из которых отвечает за свой набор кнопок.

В простых ботах хватает @bot.on_callback, но когда экранов становится много, начинаются проблемы:

  • Все callback handler’ы в одном месте
  • Ручной парсинг callback_data с if/elif
  • Нет изоляции между экранами

UIComponent решает это:

  • Автоматическая маршрутизация — callback_data "profile:toggle" попадает в ProfileScreen, а "settings:theme" — в SettingsScreen
  • Изоляция — каждый экран знает только о своих кнопках
  • Реестры — промпты и клавиатуры регистрируются декораторами
from edgebot import UIComponent, Context
class HelloScreen(UIComponent):
__prefix__ = "hello"
text = "Привет! Нажми кнопку 👇"
markup = "hello_main"
async def open(self, ctx: Context) -> None:
await self.send(ctx)
@UIComponent.callback("greet")
async def on_greet(self, ctx: Context) -> None:
await ctx.answer_callback("Привет! 👋")

Каждый UIComponent задаёт:

АтрибутНазначение
__prefix__Уникальный префикс для callback_data (обязательно)
promptКлюч в PromptRegistry для текста сообщения
markupКлюч в KeyboardRegistry для клавиатуры
textInline-шаблон текста (альтернатива prompt)
bot/prompts/profile.py
from edgebot import Prompt
from . import registry
@registry.register("profile_main")
def p_profile_main(user_id: int, username: str, is_active: bool) -> Prompt:
status = "" if is_active else ""
username_str = f"@{username}" if username else ""
return Prompt(
"👤 Профиль\n\n"
f"ID: {user_id}\n"
f"Username: {username_str}\n"
f"Активен: {status}"
)
bot/keyboards/profile.py
from edgebot import InlineKeyboard
from . import registry
@registry.register("profile_main")
def kb_profile_main(is_active: bool) -> InlineKeyboard:
toggle_text = "❌ Выключить" if is_active else "✅ Включить"
kb = InlineKeyboard()
kb.button(toggle_text, callback_data="toggle")
kb.row()
kb.button("↺ Сбросить", callback_data="reset")
kb.button("🗑 Удалить", callback_data="delete")
return kb
bot/screens/profile.py
from edgebot import Context, UIComponent
class ProfileScreen(UIComponent):
__prefix__ = "profile"
prompt = "profile_main"
markup = "profile_main"
async def open(self, ctx: Context) -> None:
user_id = ctx.from_user["id"]
user = await ctx.users.get(user_id)
if user is None:
user = await ctx.users.create(user_id, {"is_active": True})
kw = {"user_id": user_id, **user}
await self.send(ctx, prompt_kwargs=kw, markup_kwargs=kw)
@UIComponent.callback("toggle")
async def on_toggle(self, ctx: Context) -> None:
user_id = ctx.from_user["id"]
user = await ctx.users.get(user_id)
updated = await ctx.users.update(
user_id, {"is_active": not user["is_active"]}
)
kw = {"user_id": user_id, **updated}
await self.update(ctx, prompt_kwargs=kw, markup_kwargs=kw)
await ctx.answer_callback("Статус обновлён")
@UIComponent.callback("delete")
async def on_delete(self, ctx: Context) -> None:
await ctx.users.delete(ctx.from_user["id"])
await ctx.edit_message("Запись удалена. /start для регистрации.")
await ctx.answer_callback("Удалено")
bot/main.py
from edgebot import Bot, Context, UserRegistry, KeyboardRegistry, PromptRegistry
from .keyboards import registry as kb_registry
from .prompts import registry as pr_registry
from .screens import ProfileScreen
def build_bot(env) -> Bot:
profile = ProfileScreen()
return (
Bot(env.BOT_TOKEN, parse_mode="Markdown")
.with_users(UserRegistry(env.USERS))
.with_keyboards(kb_registry)
.with_prompts(pr_registry)
.register_ui(profile)
)

Паттерны пишутся БЕЗ префикса. Поддерживаются параметры через {name}:

class AdminScreen(UIComponent):
__prefix__ = "admin"
@UIComponent.callback("ban:{user_id}")
async def on_ban(self, ctx: Context, user_id: str) -> None:
# callback_data = "admin:ban:12345" → user_id = "12345"
await ctx.answer_callback(f"Пользователь {user_id} заблокирован")
@UIComponent.callback("page:{num}")
async def on_page(self, ctx: Context, num: str) -> None:
# callback_data = "admin:page:2" → num = "2"
...

Паттерн "*" ловит все callback_data с нужным префиксом, не подошедшие под другие паттерны:

class MenuScreen(UIComponent):
__prefix__ = "menu"
@UIComponent.callback("home")
async def on_home(self, ctx: Context) -> None:
...
@UIComponent.callback("*")
async def on_unknown(self, ctx: Context) -> None:
# Сработает для "menu:xyz", "menu:foo:bar" и т.д.,
# но НЕ для "menu:home" (перехвачен выше)
await ctx.answer_callback("Неизвестная команда")
МетодДействиеКогда использовать
self.send()Отправляет новое сообщениеopen(), команды
self.update()Редактирует текущее сообщениеCallback-обработчики

update() тихо проглатывает ошибку "message is not modified" — если текст и клавиатура не изменились, Telegram не жалуется.

Один и тот же dict передаётся и в фабрику промпта, и в фабрику клавиатуры. Лишние ключи автоматически отбрасываются по сигнатуре фабрики:

kw = {"user_id": 123, "username": "john", "is_active": True}
await self.send(ctx, prompt_kwargs=kw, markup_kwargs=kw)
# Промпт получит: user_id, username, is_active
# Клавиатура получит: is_active (только он объявлен в её сигнатуре)