
Taigram: универсальная клавиатура и исключения
В этой статье мы продолжаем рассказывать о нашем Open Source проекте - Taigram! Вы узнаете, как мы придумывали механизм генерации клавиатуры, сформировали пользовательское меню и как отлавливаем необработанные ошибки.
Реклама

Продолжаем рассказывать о разработке нашего Open Source проекта Taigram
.
Taigram - это Open Source Self-Hosted решение по отправке уведомлений о событиях из менеджера управления проектами Taiga в Telegram.
В этой статье мы расскажем о том, как решили переосмыслить клавиатуры в 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
) через_group_buttons_into_fixed_rows()
. - Возврат клавиатуры
• ВозвращаетсяInlineKeyboardMarkup
илиReplyKeyboardMarkup
.
- Вызывается
- Динамическая клавиатура:
- Вызывается
create_dynamic_keyboard(...)
• Получает:
•buttons_dict
(с кнопками);
•lang
,keyboard_type
;
• ключ для храненияkey_in_storage
;
• заголовокkey_header_title
;
• необязательное дополнительное действие (например,Назад
). - Подготовка структуры кнопок
• Метод_get_prepare_data_to_buttons_dict()
:
• добавляет шапку (fixed_top
), действия (fixed_bottom
), основное тело (buttons
);
• сохраняет вBUTTONS_KEYBOARD_STORAGE
. - Обработка пагинации и разбивка на страницы
• Метод_get_prepare_data_to_keyboard_data()
:
• вызываетcreate_buttons()
сdynamic
режимом;
• делит на страницы с помощью_paginate_buttons()
и_group_buttons_for_layout()
. - Построение финальной клавиатуры
• Метод_build_keyboard_rows()
собирает:
• шапку;
• текущие кнопки;
• кнопки пагинации (если нужно);
• нижние действия (например,Назад
). - Возврат клавиатуры
• ВозвращаетInlineKeyboardMarkup
илиReplyKeyboardMarkup
.
- Вызывается
Отдельно стоит упомянуть одну из ключевых архитектурных ошибок — мы решили указывать в поле data каждой кнопки полный callback, text или url. На первый взгляд — просто, понятно, прозрачно. На практике — оказалось совсем не так.
Позже мы заменили это на классы Callback’ов, и это решение принесло гораздо больше гибкости и порядка. Почему мы к этому пришли? Всё стало ясно, когда нам понадобилось изменить структуру проекта и перенести “Отслеживаемые типы событий” из раздела “Проект” в “Экземпляры проекта”. Казалось бы, мелочь, но тогда стало очевидно, насколько неудобно и хрупко было всё построено.
К тому же, Telegram ограничивает длину callback’а 64 символами. И даже при относительно простой иерархии меню мы быстро врезались в этот лимит — и ощутили всю боль.
Но почему мы вообще пошли по такому пути в первой версии?
Причин было несколько:
- Мы хотели чётко контролировать весь путь, чтобы реализовать универсальный механизм “назад на один уровень”, без хардкода маршрутов.
- Первую версию писал Виктор — тогда он ещё не знал о всех тонкостях и подводных камнях, а идея с Callback-классами просто не приходила в голову. Хотел как лучше.
- Ну и… клавиатура оказалась медленной. Очень медленной.
Как результат — клавиатура не справилась с рядом задач, но куда хуже другое: она оказалась тяжёлой в поддержке и плохо масштабировалась.
Но самое болезненное — это разлад внутри команды. Мы чувствовали бессилие. Формально всё работало, но ощущения были, будто таскаешь за собой железный куб вместо лёгкого конструктора. И становилось всё менее понятно: продолжать это тащить дальше или переписать с нуля?
Ознакомиться с этой версией клавиатуры можете тут.
Вариант №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: CallableTelegramObject, 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
Поддержать проект — звездочкой, фидбэком или идеей 🙌
Нам также было бы приятно, если бы вы положительно оценили эту статью и участвовали в обсуждении.
До встречи в следующих статьях!
Все статьи