Библиотека создана для того чтобы ее можно было использовать вместе с aiogram, а не вместо aiogram. Реализованы основные текстовые и клавиатурные виджеты, чтобы ускорить и упростить создание telegram ботов.
aiogram-renderer включает в себя:
- 100% асинхронность
- гибкую структуру, позволяющую без проблем использовать библиотеку совместно с aiogram
- поддержку как Reply так и Inline клавиатур
- удобный виджет Progress для управления загрузками
- режимы бота, доступные из разных окон
- работу с файлами
- организованную логику для управления FSM данными окон
- окна-уведомления, которые можно отправлять без использования данных в FSM
- управление видимостью виджетов через параметр show_on
- динамические наборы кнопок доступные из разных окон
Для установки, выполните команду
pip install aiogram-renderer
Для подключения библиотеки к проекту надо выполнить функцию configure_renderer, в ней вам нужно передать окна, которые будут использоваться в проекте и настроить при необходимости режимы.
async def main():
logging.basicConfig(level=logging.INFO)
await configure_renderer(
dp=dp,
# Подключаем окна
windows=[window],
# Задаем режимы бота (первый активный по умолчанию)
modes=[
BotMode(
name="mode1",
values=["off 🟥⬜️ Режим 1", "on ⬜️🟩 Режим 1"],
alert_window=alert,
),
BotMode(
name="mode2",
values=["off 🟥⬜️ Режим 2", "on ⬜️🟩 Режим 2"],
alert_window=alert,
has_custom_handler=False
)
]
)
await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot)
Для работы можно использовать 2 типа окон:
- Window - окно с состоянием State, данные хранятся в памяти FSM. Рендерятся окна с установкой стейта автоматически. Мапятся по state, подробнее в разделе.
window = Window(
Text("Тест окна"),
Panel(
Button(text="Кнопка", data="button"),
Button(text="Тоже кнопка", data="button_too"),
Mode(name="mode1"),
width=2
),
state=MainSteps.step1
)
- Alert - окно-уведомление, его данные не хранятся в FSM, соответственно на alert есть ограничения по использованию некоторых виджетов (Mode, DynamicPanel)
alert = Alert(
Area(
Bold("Тест окна", end_count=3),
"все хорошо",
sep_count=2
)
Для отправки сообщений из собранных окон с виджетами, используйте функции render из встроенного аргумента renderer с mode=RenderMode.ANSWER и др. или более быстрые answer, edit, delete_and_send, reply.
@dp.message(F.text=="/start")
async def start(message: Message, renderer: Renderer):
message, window = await renderer.render(window=window, chat_id=message.chat_id, message_id=message.message_id, mode=RenderMode.EDIT)
Note
Вы можете использовать renderer вместе с обычными методами update
@dp.message(F.text=="use1")
async def use1(message: Message, renderer: Renderer):
# Использование хендлера с Renderer
message, window = await renderer.delete_and_send(window=window, chat_id=message.chat_id, message_id=message.message_id)
@dp.message(F.text=="use2")
async def use2(message: Message):
# Использование встроенных методов окна для генерации исключельно клавиатуры или текста
text = await window.gen_text(data={"username": message.from_user.username})
await message.answer(text=text)
@dp.message(F.text=="use3")
async def use3(message: Message):
# Использование хендлера без Renderer
await message.answer(text="Все ок!")
Данные собранные через библиотеку хранятся в fsm для каждого пользователя в следующем формате:
{
"__modes__": {
"mode1": ["off 🟥⬜️ Режим 1", "on ⬜️🟩 Режим 1"],
"mode2": ["off 🟥⬜️ Режим 2", "on ⬜️🟩 Режим 2"]
},
"__dpanels__": {
"test_dp": {
"page": 2,
"text": ["1","2","3"],
"data": ["d1","d2","d3"]
}
},
"__windows__": {
"MainSteps.step1": {
"username": "Коля",
"visible": true
},
"MainSteps.step2": {
"username": "Маша",
"surname": "Сечинова",
"visible": false
}
}
}
Все окна хранятся в порядке открытия и могут перезаписываться если указывать новые параметры data при их рендере:
@dp.message(F.text=="/start")
async def start(message: Message, renderer: Renderer):
await renderer.delete_and_send(window=window,
chat_id=message.chat_id,
message_id=message.message_id,
data={"username": message.from_user.username})
Tip
Сами поля содержат в себе данные которые вы можете подставить в окно, использовав фигурные скобки в некоторых параметрах виджетов:
window = Window(
Text("Привет {username}, Тест окна"),
Panel(
Button(text="Кнопка", data="button{username}"),
Button(text="Тоже кнопка", data="button_too"),
Mode(name="mode1"),
width=2
),
state=MainSteps.step1
)
Для управления отображением виджетами используйте параметр show_on, значение берется из параметра data функции render и в дальнейшем хранится в памяти, учитывайте это и обновляйте параметр при необходимости.
window = Window(
Text("Привет {username}, Тест окна"),
Panel(
Button(text="Кнопка", data="button{username}", show_on="is_admin"),
Button(text="Тоже кнопка", data="button_too", show_on="is_admin"),
Mode(name="mode1"),
width=2
),
state=MainSteps.step1
)
@dp.message(F.text=="/start")
async def start(message: Message, renderer: Renderer):
await renderer.delete_and_send(window=window,
chat_id=message.chat_id,
message_id=message.message_id,
data={"is_admin": True})
Частенько требовалась кнопка которая бы обновляла и хранила свое активное состояние в разных сообщениях. Режимы позволяют реализовать это, укажите их в configure_renderer и потом передавайте в окна через Mode, ReplyMode виджеты. У каждого режима есть alert_window, это нужно чтобы работал системный хендлер ReplyMode, также вы можете указать has_custom_handler и настроить свою хендлер для обработки апдейта режима.
window = Window(
Text("Привет {username}, Тест окна"),
ReplyPanel(
ReplyButton(text="Кнопка", data="button{username}"),
ReplyButton(text="Тоже кнопка", data="button_too", show_on="is_admin"),
ReplyMode(name="mode2"),
width=2
),
state=MainSteps.step1
)
async def main():
logging.basicConfig(level=logging.INFO)
await configure_renderer(
dp=dp,
# Подключаем окна
windows=[window],
# Задаем режимы бота (первый активный по умолчанию)
modes=[
BotMode(
name="mode1",
values=["off 🟥⬜️ Режим 1", "on ⬜️🟩 Режим 1"],
),
BotMode(
name="mode2",
values=["Админ", "Юзер", "Продавец"],
alert_window=alert,
has_custom_handler=True
)
]
)
await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot)
@dp.message(IsMode("mode2"))
async def update_mode(message: Message, renderer: Renderer):
# Получаем режим с помощью функции get_mode_by_value (также есть get_mode_by_name)
mode = await renderer.bot_modes.get_mode_by_value(value=message.text)
# Обновляем режим
await renderer.bot_modes.update_mode(mode=mode.name)
# Обновляем окно
await renderer.edit(window=MenuStates.step1, chat_id=pr_message.chat.id,
message_id=pr_message.message_id)
Для управления полосой загрузки используйте виджет Progress и функцию update_progress, задайте нужный интервал обновления.
window = Window(
Text("Привет {username}, Тест окна", end_count=2),
Progress(name="test_pr")
Panel(
Button(text="Кнопка", data="button{username}"),
Button(text="Тоже кнопка", data="button_too", show_on="is_admin"),
Mode(name="mode1"),
width=2
),
state=MainSteps.step1
)
@dp.message(F.text=="/start")
async def start(message: Message, renderer: Renderer):
data = {"username": message,from_user.username, "test_pr": 0}
# Сначала будет установлен процент 0
pr_message, window = await renderer.answer(
window=MenuStates.step1,
chat_id=message.chat.id,
data=data,
)
for i in range(99):
await renderer.update_progress(window=MenuStates.step1, chat_id=pr_message.chat.id, interval=0.3,
message_id=pr_message.message_id, name="test_pr", percent=i, data=data)
await sleep(0.3)
Для отображения кнопок со своими данными используется виджет DynamicPanel. Нужно указать text, data и первую активную страницу, данные сохранятся в FSM и будут доступны для всех окон.
window = Window(
Text("Привет {username}, Тест окна", end_count=2),
DynamicPanel(name="dpanel", hide_number_pages=True),
state=MainSteps.step1
)
@dp.message(F.text=="/start")
async def start(message: Message, renderer: Renderer):
data = {
"dpanel": {
"page": 2,
"text": ["1", "2", "3", "4", "5"],
"data": ["d1", "d2", "d3", "d4", "d5"]
},
}
await renderer.answer(
window=MenuStates.step1,
chat_id=message.chat.id,
data=data,
)
@dp.callback_query(F.data.startswith("d"))
async def dpanel_opt(callback: CallbackQuery, renderer: Renderer):
# Тут ваши действия для обработки кнопок из DynamicPanel
pass
Для работы с файлами реализовано 2 виджета, первый это виджет File и его дочерние Photo, Video, Audio. В нем мы указываем путь к файлу и его название.
window = Window(
Text("Привет {username}, Тест окна", end_count=2),
Audio(file_name="audio.mp3", path="audio.mp3"),
state=MainSteps.step1
)
Второй это FileBytes, он используется для передачи файла в байтах, данные не хранятся в памяти и отображаются только тогда когда вы передаете параметр file_bytes в функции render.
window = Window(
Text("Привет {username}, Тест окна", end_count=2),
AudioBytes(file_name="audio.mp3", bytes_name="bytes_a"),
state=MainSteps.step1
)
@dp.message(F.text=="/start")
async def start(message: Message, renderer: Renderer):
async with aiofiles.open(file="audio.mp3", mode="rb") as f:
await renderer.answer(
window=MenuStates.step1,
chat_id=message.chat.id,
file_bytes={"bytes_a": await f.read()},
)
Медиа группа имеет специфичную логику работы, так что пока она в разработке, нужно понять как ее правильно добавить в экосистему библиотеки.
В документации указаны не все виджеты, с остальными предлагаю ознакомиться самостоятельно 😉 По мере поддержки или при необходимости в работе буду добавлять новые виджеты и совершенствовать библиотеку, кому интересно. Так в планах MediaGroup, ReplyDynamicPanel, поддержка виджетов в Alert, виджет Calendar и поддержка доп. полей для кнопок, которые задаются в aiogram. Также возможно сделаю виджет DynamicMediaGroup. Все по мере возможностей, спасибо что прочитали.