Продолжаем рассказывать о разработке нашего Open Source проекта Taigram.
Taigram - это Open Source Self-Hosted решение по отправке уведомлений о событиях из менеджера управления проектами Taiga в Telegram.
Статьи о разработке Taigram:
Taigram: универсальная клавиатура и исключения
В этой статье мы расскажем о том, как решили переосмыслить клавиатуры в Telegram и реализовали разделение уровней доступа для отслеживания событий. И как мы реализовали универсальный обработчик ошибок из FastAPI и Aiogram.

Бета-тест
Проект уже доступен для использования!
Нам бы хотелось привлечь как можно больше внимания к менеджеру управления задачами Taiga.io и нашему решению по отправке уведомлений Taigram.
Если вы используете Taiga и хотите отправлять уведомления о событиях не на электронную почту, а в разные Telegram-чаты - попробуйте наше решение.
Проект доступен на Github. В README на русском и английском языках описан процесс быстрого запуска на своём сервере.
Коварная клавиатура
С клавиатурой у нас вышла не самая приятная ситуация. Если говорить обтекаемо, то мы не могли прийти к единому решению.
Как обычно делают клавиатуры в Telegram-ботах?
Создают отдельный модуль и в нём создают функцию, возвращающую объект клавиатуры, а-ля:
# handlers.py
@example_router.message()
async def example_handler(message: Message) -> None:
await message.answer(text="Привет!", reply_markup=hello_kb())
# keyboards.py
def hello_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text="Нажми меня", url="https://pressanybutton.ru")
return builder.as_markup()
То есть, прописывают каждую клавиатуру отдельно в коде, что, безусловно, упрощает процесс разработки. Однако, такой подход "не универсальный", если нужно добавить кнопку - идёшь в код.
Наша идея
Наша идея заключалась в том, чтобы сделать универсальный класс-генератор, в который мы могли бы передавать данные на основе которых создавался бы объект клавиатуры.
Когда мы подошли к разработке клавиатуры (после того, как закончили с инициализацией и первичной настройкой проекта), то решили сформулировать задачи, которые должна решать наша клавиатура, чтобы быть универсальной.
Поддержка мультиязычности - с учетом того, что проект OpenSource и при расширении локализации должна быть возможность удобно добавить поддержку новых языков;
Удобство при редактировании текста кнопок и структуры клавиатур;
Возможность переиспользования кнопок в разных клавиатурах и разных сценариях;
Создание как статических (заранее предопределенных), так и динамических клавиатур (содержимое которых нам заранее не известно);
Поддержка пагинации;
Удобство для дальнейшей разработки.
Но всё пошло совсем не по плану.
Вариант №1
Изначальный план сводился к тому, что мы создаем общий, универсальный класс клавиатуры, который инициализируется в нашем синглтоне. Это показалось нам хорошим решением и в последствии вошло в итоговый вариант клавиатуры.
Структура файлов для создания клавиатур
Для решения 1-й задачи (поддержке мультиязычности) мы решили, что у нас будет несколько .yaml файлов:
файл с кнопками, который содержит примерно такую структуру данных:
buttons: get_start: text: "get_start" type: "callback" data: "start"
поле
buttons
нам необходимо для того, чтобы определить корректный путь к файлу и разделу (о текстовой утилите мы подробно рассказывали в одной из предыдущих статей);поле
get_start
определяет название кнопки;поле
text
содержит в качестве значения ключ для поляkeyboard_text.yaml
(но с учетом того, что поддерживаемых языков может быть много, то утилита также определит системный язык, установленный для пользователя и найдет текст сообщения указанный в полеtext
на необходимом языке);поле
type
, в последствии будет упразднено, но в текущей реализации указывает какой тип кнопки подразумевается (допустимые форматыcallback
,reply
,url
);поле
data
, в последствии будет кардинально изменено, но в текущей реализации указывает какой callback или текст или ссылка (в зависимости от формата) будет соответствовать кнопке;
файл со списком статических клавиатур, который содержит подобные данные:
main_menu_keyboard: key: - "get_admin_menu" - "projects_menu" - "profile_menu" - "get_instructions" keyboard_type: "inline"
файл с названием кнопок, который учитывает системный язык пользователя:
ru: get_start: "Начать"
Мы можем добавлять поля 1-го уровня вложенности для определения языка, а для полей 2-го уровня вложенности у нас идет пара ключ/значения.
Предыстория класса клавиатуры
Когда мы начали разрабатывать клавиатуру, ключевая идея заключалась в создании универсального класса. Повторюсь: универсальность была в приоритете, поэтому первая реализация вышла объёмной — 831 строка кода. Хотя стоит уточнить: это вместе с тайпхинтами и докстрингами.
С самого начала мы закладывали принцип изоляции: каждый метод отвечает строго за одну задачу. Тогда Виктору это решение казалось удачным — такая структура, по его мнению, упростила бы вхождение в проект и упростила бы поддержку кода. Но в команде оно вызвало неоднозначную реакцию.
Практика показала, что чрезмерное дробление ответственности может сыграть и против нас. В ряде случаев, чтобы внести правку в логику, приходилось каскадно менять множество мест, лишь бы передать нужный аргумент до нужного метода в нужном экземпляре класса.
Алгоритм работы (обобщенный)
Получение ключа клавиатуры / данных от пользователя • Метод вызывается извне с ключом (key) для статической клавиатуры или с buttons_dict — для динамической. • Также передаются язык (lang) и, опционально, placeholder — подставляемые значения в шаблоны callback’ов.
Статическая клавиатура:
Вызывается
create_static_keyboard(key, lang, placeholder)
Получает данные по ключу
key
изget_strings()["keyboards_list"][key]
;Проверяет тип клавиатуры —
inline
илиreply
;Получает список кнопок через
data.get("key")
;Вызывает
create_buttons()
с нужным типом и режимомstatic
.
Формируется список кнопок
Каждая кнопка создаётся из YAML-описания (
get_button_info(key)
).Применяется перевод (
translate_button_text()
).Используется
format_text_with_kwargs()
для подстановки значений изplaceholder
.
Группировка кнопок
Кнопки группируются в строки с нужной шириной (
row_width
) через_groupbuttons_into_fixed_rows()
.
Возврат клавиатуры
Возвращается
InlineKeyboardMarkup
илиReplyKeyboardMarkup
.
Динамическая клавиатура:
Вызывается
create_dynamic_keyboard(...)
Получает:
buttons_dict
(с кнопками);lang
,keyboard_type
;ключ для хранения
key_in_storage
;заголовок
key_header_title
;необязательное дополнительное действие (например,
Назад
).
Подготовка структуры кнопок
Метод
_getprepare_data_to_buttons_dict()
:добавляет шапку (
fixed_top
), действия (fixed_bottom
), основное тело (buttons
);сохраняет в
BUTTONS_KEYBOARD_STORAGE
.
Обработка пагинации и разбивка на страницы
Метод
_getprepare_data_to_keyboard_data()
:вызывает
create_buttons()
сdynamic
режимом;делит на страницы с помощью
paginatebuttons()
и_groupbuttons_for_layout()
.
Построение финальной клавиатуры
Метод
_buildkeyboard_rows()
собирает:шапку;
текущие кнопки;
кнопки пагинации (если нужно);
нижние действия (например,
Назад
).
Возврат клавиатуры
Возвращает
InlineKeyboardMarkup
илиReplyKeyboardMarkup
.
Отдельно стоит упомянуть одну из ключевых архитектурных ошибок — мы решили указывать в поле data каждой кнопки полный callback, text или url. На первый взгляд — просто, понятно, прозрачно. На практике — оказалось совсем не так.
Позже мы заменили это на классы Callback’ов, и это решение принесло гораздо больше гибкости и порядка. Почему мы к этому пришли? Всё стало ясно, когда нам понадобилось изменить структуру проекта и перенести “Отслеживаемые типы событий” из раздела “Проект” в “Экземпляры проекта”. Казалось бы, мелочь, но тогда стало очевидно, насколько неудобно и хрупко было всё построено.
К тому же, Telegram ограничивает длину callback’а 64 символами. И даже при относительно простой иерархии меню мы быстро врезались в этот лимит — и ощутили всю боль.
Но почему мы вообще пошли по такому пути в первой версии? Причин было несколько:
1. Мы хотели чётко контролировать весь путь, чтобы реализовать универсальный механизм “назад на один уровень”, без хардкода маршрутов.
2. Первую версию писал Виктор — тогда он ещё не знал о всех тонкостях и подводных камнях, а идея с Callback-классами просто не приходила в голову. Хотел как лучше.
3. Ну и… клавиатура оказалась медленной. Очень медленной.
Как результат — клавиатура не справилась с рядом задач, но куда хуже другое: она оказалась тяжёлой в поддержке и плохо масштабировалась.
Но самое болезненное — это разлад внутри команды. Мы чувствовали бессилие. Формально всё работало, но ощущения были, будто таскаешь за собой железный куб вместо лёгкого конструктора. И становилось всё менее понятно: продолжать это тащить дальше или переписать с нуля?
Ознакомиться с этой версией клавиатуры можете тут.
Вариант №2
После всех этих проблем Ваня решил пересмотреть подход к разработке и использованию клавиатуры. Но, если однажды что-то увидел — развидеть уже невозможно. Идейно мы остались верны изначальной концепции, просто убрали лишнее и усилили то, что действительно работало.
Клавиатура по-прежнему строится по знакомым принципам:
• Разделение конфигураций по .yaml-файлам никуда не делось — это оказалось удобно.
• Мы по-прежнему обрабатываем клавиатуры по типам: статические и динамические.
• Есть единый экземпляр класса клавиатуры, реализованный через синглтон — он управляет всем взаимодействием внутри.
Проект не переписывался с нуля, но стал проще, стройнее и — главное — устойчивее. Мы отказались от догматизма и сосредоточились на практической пользе. Именно это стало основой для следующей версии.
Структура файлов для создания клавиатур
Файл с кнопками:
get_main_menu: text: get_main_menu callback_class: MenuData
Раньше мы указывали type и data, теперь объединили их в одно универсальное поле — callback_class. Такая схема проще и выразительнее: за всю логику теперь отвечают Callback-классы, а не текстовые коллбеки, набитые руками.
Что это дало:
Мы полностью ушли от хардкода callback’ов;
Роутеры стали проще: не нужно больше «парсить» строки и вытаскивать из них суть;
Код стал понятнее и безопаснее — меньше шансов ошибиться в одном символе и получить неожиданный результат;
Мы упростили фильтрацию в роутерах, потому что нам не нужно "парсить" и обрабатывать коллбек.
Файл с языками:
Раньше все языки хранились в одном огромном YAML-файле. Пока у нас было 2 языка — всё шло гладко. Но стоило задуматься о масштабировании — и стало ясно: такой подход не выживет.
Теперь у нас есть точка входа:
ru: !include lang/ru/keyboard_text.yaml en: !include lang/en/keyboard_text.yaml
Каждый язык — в своём отдельном файле. Это упростило как поддержку, так и внесение изменений. Локализации теперь можно расширять буквально одной строкой.
В остальном, все осталось без существенных изменений.
Файлы с клавиатурами:
Ранее у нас был 1 файл со статическими клавиатурами и отдельные файлы с динамическими клавиатурами для каждого модуля. Это тоже оказалось неэффективным и поэтому мы пришли к выводу, что лучше сделать:файл со статическими клавиатурами;
файл с динамическими клавиатурами;
файл с "чекбокс" клавиатурами (это наша маленькая гордость, которую придумал Виктор Королев (он один из тех, кто захотел присоединиться к разработке нашего проекта)).
Файл со статическими клавиатурами:
Мы решили также "включать" данные из сторонних .yaml
файлов, чтобы:
Упростить процесс взаимодействия с кнопками;
Получить возможность при необходимости указывать аргументы, которые требуют конкретные callback классы;
Получить возможность переопределять конкретные поля для любой из кнопок (например, мы часто использовали кнопку "Назад", но по-умолчанию ей соответствует текст "Назад", в то время как где-то уместнее использовать "Отмена" или "В меню". Или если необходимо переопределить Callback класс).
Как это выглядит в коде:
buttons: !include keyboard_buttons.yaml
remove_admin_menu:
buttons_list:
- - ref: remove_admin_confirm
- - ref: cancel
callback_class: AdminManageData
args: [ "id" ]
keyboard_type: "inline"
Пример статической клавиатуры:

Файл с динамическими клавиатурами:
Во многом повторяет структуру файла статических клавиатур, за исключением, что у нас добавлены поля header_text
, data_args
, data_text_field
, pagination_class
.
В поле header_text
мы указываем ключ для текста, который будет размещен в заголовке клавиатуры (о внешнем виде меню мы расскажем дальше).
В поле data_args
мы указываем аргументы для динамических кнопок, которые будут сгенерированы. Эти аргументы ожидаются в конкретном Callback классе.
В поле data_text_field
мы указываем как будут называться динамические кнопки, которые будут сгенерированы.
В поле buttons_list
мы указываем также список обязательных кнопок, которые должны быть в каждой динамической клавиатуре.
buttons: !include keyboard_buttons.yaml
admin_menu:
header_text: "admins_menu"
data_callback: AdminManageData
data_args: ["id"]
data_text_field: "full_name"
buttons_list:
-
- ref: get_main_menu
- ref: add_admin
pagination_class: AdminMenuData
Пример динамической клавиатуры:

Файл с "чекбокс" клавиатурами:
Основная идея этой клавиатуры заключается в том, что у нас есть какое-то количество событий, которые можно отслеживать в рамках конкретного проекта. Но как мы рассказали в предыдущей статье мы решили пойти дальше и сделать "разделение уровней доступа", добавив возможность создавать "экземпляры" в рамках проекта и для каждого экземпляра пользователь может указать свой чат/топик в Telegram.
Для того, чтобы удобно управлять какие типы событий должны отслеживаться для конкретного экземпляра, мы подумали, что будет круто, если можно будет в одном меню получить доступ ко всем возможным событиям без необходимости создавать и заходить в отдельные меню, чтобы активировать/деактивировать отслеживания типа событий.
Так, если какой-то тип не отслеживается, то возле него пустой квадратик, а если отслеживается, то там галочка.
Мы также тут включаем статические кнопки, поскольку чекбокс клавиатура это "модифицированная динамическая клавиатура".
buttons: !include keyboard_buttons.yaml
edit_fat_keyboard:
items:
- "edit_project_instance_fat_epic_event"
- "edit_project_instance_fat_milestone_event"
- "edit_project_instance_fat_userstory_event"
- "edit_project_instance_fat_task_event"
- "edit_project_instance_fat_issue_event"
- "edit_project_instance_fat_wikipage_event"
- "edit_project_instance_fat_test"
ids:
- 0
- 1
- 2
- 3
- 4
- 5
- 6
buttons_list:
- - ref: edit_particular_instance
text: go_back
Пример чекбокс клавиатуры:

Алгоритм работы (обобщенный)
Статическая клавиатура
Получение конфигурации по ключу:
self._static_keyboards.get(kb_key)
.
Определение типа клавиатуры:
по полю
keyboard_type
→INLINE
илиREPLY
.
Инициализация билдера:
InlineKeyboardBuilder()
илиReplyKeyboardBuilder()
.
Создание кнопок:
await _generate_static_buttons_row(...)
:перебирает
buttons_list
;вызывает
_get_static_inline_button
или_get_static_reply_button
;подставляет переводы;
добавляет
KeyboardButtonRequestUsers
, еслиrequest_users=True
.
Опционально добавляется меню-кнопка:
_get_menu_button()
— кнопка “В главное меню”.
Формирование объекта клавиатуры:
builder.as_markup(...)
.
Динамическая клавиатура
Получение конфигурации по ключу из
dynamic_keyboards
.Инициализация
InlineKeyboardBuilder
.Создание заголовка (опционально):
_generate_keyboard_header()
создаёт строку из 3 кнопок: пустая, заголовок, пустая.
Создание кнопок из данных:
_generate_dynamic_buttons(...)
:перебирает список
data
;для каждой строки:
достаёт
text_field
(например, project.name);собирает
args
в словарь;создаёт
InlineKeyboardButton
.
Добавление пагинации (если
count > page_limit
):_get_pagination_buttons(...)
:вычисляет общее количество страниц;
добавляет ⬅️ текущая страница ➡️.
Добавление нижних статических кнопок:
также через
_generate_static_buttons_row
.
Меню-кнопка — опционально.
Финальная клавиатура:
builder.as_markup()
.
Чекбокс клавиатура
Получение конфигурации по ключу из
checkbox_keyboards
.Создание списка чекбоксов:
items = list(zip(texts, ids))
— отображение текста и id;для каждого:
определяется, выбран ли item;
формируется текст с ✅ или ⬜;
создаётся
callback_data
наCheckboxData
.
Добавляется кнопка подтверждения (OK):
с
action="confirm"
и текущимиselected_ids
.
Добавляются нижние кнопки (если указаны).
Возврат
InlineKeyboardMarkup
.
Ознакомиться с этой версией клавиатуры можете тут.
Внедрение Зависимостей (DI)
Клавиатура применяется практически ко всем исходящим от бота сообщениям, следовательно, нужно в каждом обработчике обращаться к экземпляру класса.
Для упрощения мы применили метод Внедрения Зависимостей (Dependency Injection). В aiogram он реализовывается достаточно просто.
Всё, что нам необходимо, это создать Middleware, который будет добавлять в каждый обработчик объект клавиатуры (а также объект пользователя).
class DependencyMiddleware(BaseMiddleware):
"""
Middleware for dependency injection in Telegram bot handlers.
"""
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
data["user"] = await UserService().get_or_create_user(user=data.get("event_from_user"))
data["keyboard_generator"] = KeyboardGenerator()
return await handler(event, data)
Это позволило "забыть" о получении объекта класса в обработчике, поскольку в каждом теперь есть экземпляр:
@main_router.message(Command(commands=[CommandsEnum.START]))
async def start_handler(
message: Message, state: FSMContext, user: UserSchema, keyboard_generator: KeyboardGenerator
) -> None:
Формат меню
Меню управления ботом тоже много обсуждалось. Мы хотели сделать удобное управление подключениями, и вот, что у нас получилось.
Раздел "Администраторы"
В этом разделе можно получить информацию о добавленных администраторах, а также добавлять их и удалять.
Внешний вид меню:

В этом меню:
Указывается количество администраторов
В самом верху клавиатуры, указано имя раздела, которое получаем из поля
header_text
при генерации.Далее клавиши администраторов генерируемые динамически и ведущие в их меню.
В низу две статические клавиши.
Добавление администраторов
Если с информацией и удалением всё понятно, то на добавлении давайте остановимся подробнее.
У нас было несколько вариантов, как добавлять администраторов в бота:
Генерацией пригласительной ссылки, перейдя по которой пользователь бы получал админ-права
Прописыванием Telegram-ID пользователя с занесением в специальный список, по которому была бы валидайция прав
Выбор из активировавших бота пользователей.
Варианты интересные, но все они показались нам несостоятельными. Первый потребовал бы лишних действий со стороны пользователей. Второй не оптимален, а третий излишне громоздкий.
И тут нам пришла идея воспользоваться специальным аргументом у Reply-клавиатуры
- request_users
. Суть этого аргумента заключается в том, что у пользователя (вернее администратора, но со стороны ТГ это всё пользователи) появляется кнопка, нажав которую отображается выбор контактов. Выбрав нужных пользователей, мы в боте получаем их ID, а также имя и заносим в БД как администраторов. Теперь, когда добавленный пользователь зайдёт в бота, у него уже будет учётная запись и он будет с выданными админ-правами.



Раздел "Проекты"
В разделе "Проекты", добавляются проекты из Тайги. Подразумевается, что если у вас несколько проектов в тайге, то для каждого будет добавлен своя запись в Taigram.
Внутри проекта можно добавить так называемые "инстансы" (экземпляры), о них мы частично рассказали выше, когда описывали суть чекбокс клавиатуры.
Каждый инстанс выдаёт уникальную ссылку для добавления в список вебхуков в Тайге и отправляет уведомления в один чат с указанными настройками уведомлений по типам (Задача, Спринт, Пользовательская история, Запрос и т.д.). Тем самым можно разделить отправку уведомлений, например по задачам в один чат, а по обновлениям Вики в другой и всё это для одного проекта.
Поскольку изначально мы задумывали экземпляры проекта как способ разделения уровней доступа, то одним из ценных сценариев создания экземпляров мы видим такой:
Руководство отдела или какие-нибудь СЕО (топ-менеджемент) хотят быть в курсе событий, но залезать каждый раз на доску и отслеживать изменения им может быть не удобно. В таком случае под них ПМ создает отдельный экземпляр и указывает, что для них нужно отслеживать обновления эпиков, чтобы не пропустить самое главное;
Ребятам, занимающимся дизайном совершенно не интересно какие решения придумали бэкенд разработчики и получать уведомления о их достижениях они тоже не хотят. Для этого ПМ создает отдельный экземпляр и указывает, что для них нужно отслеживать конкретный спринт или юзер-стори;
К сожалению, пока что реализован только 1-й сценарий, поскольку для второго нам не хватает информации о том, насколько это удобно и нужно реальному пользователю. На основе такой информации мы бы смогли сделать такой функционал, который и правда будет полезен, а не просто функционал ради функционала.
Внешний вид меню:
Мы стремились сделать интерфейс максимально простым и понятным, насколько это вообще возможно при работе с ботами. Главное правило — не проваливаться во вложенные меню глубже, чем Алиса в Страну чудес.
Особенно это касается динамических клавиатур. Именно там проще всего скатиться в хаос и нагромождение уровней. Чтобы этого избежать, мы выработали несколько простых и чётких принципов:
Всегда есть заголовок. Он идёт первым и помогает пользователю понять, где он находится.
Показываем ровно 5 сгенерированных кнопок. Больше — уже визуальный шум, меньше — ощущение пустоты.
В нижнем левом углу — кнопка “Назад”. Она возвращает на предыдущий уровень. Никаких догадок, всё предсказуемо.
В правом нижнем углу — кнопка “Добавить”. В зависимости от контекста это может быть «Добавить проект», «Добавить экземпляр», «Добавить администратора» и т. д.
Если записей больше 5 — добавляется строка пагинации. Без неё теряется управляемость, а с ней — пользователь всегда видит, что есть ещё страницы.
Такой подход позволил нам сохранить визуальную лёгкость, понятную структуру и избежать UX-хаоса. А главное — пользователи бота не задают вопросов вроде «где я?» и «как отсюда выйти?».

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

После успешного добавления проекта, пользователь получает выбор:
Перейти к редактированию — и сразу начать настраивать проект, добавлять экземпляры, администраторов и т.д.
Вернуться в меню — если хочет продолжить работу с другими разделами.
Мы сознательно сделали этот шаг максимально простым: минимум обязательных полей, никаких лишних экранов и диалогов. Всё ради того, чтобы не сбивать темп работы и не терять контекст.

Добавление экземпляров
Чтобы добавить экземпляр, пользователь должен сначала открыть нужный проект. Это можно сделать сразу после создания проекта или позже — через пункт меню «Проекты».
Мы сознательно не усложняли процесс: интерфейс построен так, чтобы пользователь интуитивно понимал, что действия с экземплярами следуют той же логике, что и с проектами или администраторами.
Добавил проект → видишь кнопку «Добавить экземпляр» — и дальше всё уже знакомо.
Такая единообразная структура помогает «приучить» пользователя к интерфейсу и снять лишнюю когнитивную нагрузку.

Редактирование экземпляров
Мы прекрасно понимали: если дать пользователю возможность что-то добавить, то точно так же нужно дать ему возможность этим управлять.
Поэтому мы реализовали простое и логичное меню управления, где пользователь может:
Получить ссылку для добавления в Taiga;
Получить информацию об экземпляре;
Изменить название экземпляра;
Изменить источники для отправки;
Изменить типы отслеживаемых событий;
Удалить экземпляр проекта.

Добавление чатов в экземпляры
Ранее мы уже несколько раз упоминали экземпляры (инстансы) — пора подробнее рассказать, зачем они вообще нужны.
Экземпляр — это сущность, которая позволяет:
Определить, куда улетать уведомлениям. То есть, в какой чат или в какую тему внутри Telegram.
Настроить фильтрацию событий. Какие именно типы событий отслеживать, чтобы по ним формировать уведомления: задачи, баги, комментарии и так далее.
Таким образом, экземпляры выступают прослойкой между проектом и конечной точкой доставки уведомлений, позволяя гибко настраивать поведение под разные команды, проекты и сценарии использования.

Добавление типов отслеживаемых событий в экземпляры
Ранее мы уже упоминали нашу маленькую гордость — чекбокс-клавиатуры, и даже показывали, как они выглядят. Но именно сейчас этот элемент начинает играть ключевую роль.
Когда пользователь создаёт экземпляр проекта, он может сразу же настроить, какие типы событий следует отслеживать. И здесь в игру вступает чекбокс-клавиатура.
В рамках одного меню пользователь:
Видит список всех доступных типов событий (например: создание задачи, изменение статуса, комментарии и т.п.);
Может включать или отключать их одним нажатием, прямо в интерфейсе — без переходов, без подтверждений, без лишнего шума.
Никаких подменю, никаких “Сохранить”, никакого лишнего клика — всё работает в реальном времени и максимально интуитивно. Именно этого мы добивались: чтобы взаимодействие с ботом ощущалось как работа с нативным UI, а не как хождение по вложенным слоям настроек.

"Отлов" ошибок
В любом проекте неизбежны ошибки и не все из них получается обработать сразу. Для этого в проекте есть целых два обработчика ошибок:
Обработчик ошибок от FastAPI
Обработчик ошибок от aiogram
Для чего они нужны и зачем два?
Если в коде происходит какое-то непредвиденное изначально событие (исключение), то нужно как-то уведомить об этом администратора (или отправить в специальный чат для уведомлений).Это позволяет своевременно отреагировать до того, как прилетит сообщение от пользователя, что "что-то не работает".
Поскольку FastAPI и aiogram работают хоть и вместе, но всё таки независимо друг от друга, то вызываемые в процессе работы исключения каждый их них обрабатывает только свои.
Обработчик ошибок FastAPI
FastAPI предоставляет "из коробки" метод-декоратор exception_handler
. Единственный минус. нужно указывать конкретное исключение которое он обрабатывает, или обходиться базовым Exception
.
@app.exception_handler(MessageFormatterError)
async def handle_exception(request: Request, exc: MessageFormatterError):
logger.critical("Error: %s", exc.message, exc_info=True)
await send_message(
chat_id=get_settings().ERRORS_CHAT_ID,
message_thread_id=get_settings().ERRORS_THREAD_ID,
text=get_service_text(text_in_yaml="error_message", exception=exc.message),
)
В данном обработчике мы отслеживаем ошибку MessageFormatterError
. Это кастомный обработчик для нашего модуля формирования текста (о котором было в статье ...), срабатывающий при поступлении некорректных или пустых (в плане информативности) данных.
Записываем исключение в лог и отправляем сообщение администратору (или в чат для ошибок).
Обработчик ошибок aiogram
Точно также, aiogram предоставляет обработчик ошибок и "из коробки", но в отличии от FastAPI, он срабатывает на все исключения вызванные в процессе работы бота.
@service_errors_router.error()
async def error_handler(event: ErrorEvent) -> None:
logger.critical("Error: %s", event.exception, exc_info=True)
await send_message(
chat_id=get_settings().ERRORS_CHAT_ID,
message_thread_id=get_settings().ERRORS_THREAD_ID,
text=get_service_text(text_in_yaml="error_message", exception=event.exception),
)
В остальном принцип работы у них одинаков.
Пример оповещения:

Заключение
Разработка Taigram — это история постоянного переосмысления, поиска удобных решений и адаптации под реальные сценарии использования. Мы не просто хотели «сделать, чтобы работало», а стремились к тому, чтобы было удобно, гибко и масштабируемо. Клавиатуры стали для нас настоящим вызовом: от простых функций с кнопками до продуманной архитектуры, где каждая кнопка живёт своей YAML-жизнью.
Мы старались учесть всё: поддержку языков, переиспользуемость компонентов, отказ от хардкода и постепенный переход к подходу, в котором менять структуру становится не больно, а приятно. И хотя не всё получалось с первого раза, и путь к удобному решению оказался нелёгким, мы уверены — результат того стоит.
Добавление администраторов с помощью request_users
, кастомные Callback-классы, разделение уровней доступа для уведомлений, универсальный обработчик ошибок — всё это стало неотъемлемой частью системы. Мы сделали всё, чтобы Taigram был не просто ботом, а надёжным нотификатором для Taiga.
И как бы пафосно это ни звучало, нам действительно важно сделать Open Source продукт, которым удобно пользоваться и который хочется развивать. Если вы используете Taiga — попробуйте Taigram. Если нет — может, самое время начать? 😉
Попробовать Taigram на GitHub
Поддержать проект — звездочкой, фидбэком или идеей 🙌
Нам также было бы приятно, если бы вы положительно оценили эту статью и участвовали в обсуждении.
До встречи в следующих статьях!