Taigram: универсальная клавиатура и исключения

от автора

Продолжаем рассказывать о разработке нашего Open Source проекта Taigram.

Taigram — это Open Source Self-Hosted решение по отправке уведомлений о событиях из менеджера управления проектами Taiga в Telegram.

Статьи о разработке Taigram:

  1. Taigram: Начало работы

  2. Taigram: Архитектура приложения

  3. Taigram: как мы решали проблемы данных и пришли к бете

  4. 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()

То есть, прописывают каждую клавиатуру отдельно в коде, что, безусловно, упрощает процесс разработки. Однако, такой подход «не универсальный», если нужно добавить кнопку — идёшь в код.

Наша идея

Наша идея заключалась в том, чтобы сделать универсальный класс-генератор, в который мы могли бы передавать данные на основе которых создавался бы объект клавиатуры.

Когда мы подошли к разработке клавиатуры (после того, как закончили с инициализацией и первичной настройкой проекта), то решили сформулировать задачи, которые должна решать наша клавиатура, чтобы быть универсальной.

  1. Поддержка мультиязычности — с учетом того, что проект OpenSource и при расширении локализации должна быть возможность удобно добавить поддержку новых языков;

  2. Удобство при редактировании текста кнопок и структуры клавиатур;

  3. Возможность переиспользования кнопок в разных клавиатурах и разных сценариях;

  4. Создание как статических (заранее предопределенных), так и динамических клавиатур (содержимое которых нам заранее не известно);

  5. Поддержка пагинации;

  6. Удобство для дальнейшей разработки.

Но всё пошло совсем не по плану.

Вариант №1

Изначальный план сводился к тому, что мы создаем общий, универсальный класс клавиатуры, который инициализируется в нашем синглтоне. Это показалось нам хорошим решением и в последствии вошло в итоговый вариант клавиатуры.

Структура файлов для создания клавиатур

Для решения 1-й задачи (поддержке мультиязычности) мы решили, что у нас будет несколько .yaml файлов:

  1. файл с кнопками, который содержит примерно такую структуру данных:

    buttons:   get_start:     text: "get_start"     type: "callback"     data: "start"
    • поле buttons нам необходимо для того, чтобы определить корректный путь к файлу и разделу (о текстовой утилите мы подробно рассказывали в одной из предыдущих статей);

    • поле get_start определяет название кнопки;

    • поле text содержит в качестве значения ключ для поля keyboard_text.yaml (но с учетом того, что поддерживаемых языков может быть много, то утилита также определит системный язык, установленный для пользователя и найдет текст сообщения указанный в поле text на необходимом языке);

    • поле type, в последствии будет упразднено, но в текущей реализации указывает какой тип кнопки подразумевается (допустимые форматы callback, reply, url);

    • поле data, в последствии будет кардинально изменено, но в текущей реализации указывает какой callback или текст или ссылка (в зависимости от формата) будет соответствовать кнопке;

  2. файл со списком статических клавиатур, который содержит подобные данные:

    main_menu_keyboard:   key:     - "get_admin_menu"     - "projects_menu"     - "profile_menu"     - "get_instructions"   keyboard_type: "inline"
  3. файл с названием кнопок, который учитывает системный язык пользователя:

    ru:   get_start: "Начать"

    Мы можем добавлять поля 1-го уровня вложенности для определения языка, а для полей 2-го уровня вложенности у нас идет пара ключ/значения.

Предыстория класса клавиатуры

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

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

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

Алгоритм работы (обобщенный)

  1. Получение ключа клавиатуры / данных от пользователя •Метод вызывается извне с ключом (key) для статической клавиатуры или с buttons_dict — для динамической. •Также передаются язык (lang) и, опционально, placeholder — подставляемые значения в шаблоны callback’ов.

  2. Статическая клавиатура:

    1. Вызывается create_static_keyboard(key, lang, placeholder)

      • Получает данные по ключу key из get_strings()["keyboards_list"][key];

      • Проверяет тип клавиатуры — inline или reply;

      • Получает список кнопок через data.get("key");

      • Вызывает create_buttons() с нужным типом и режимом static.

    2. Формируется список кнопок

      • Каждая кнопка создаётся из YAML-описания (get_button_info(key)).

      • Применяется перевод (translate_button_text()).

      • Используется format_text_with_kwargs() для подстановки значений из placeholder.

    3. Группировка кнопок

      • Кнопки группируются в строки с нужной шириной (row_width) через _groupbuttons_into_fixed_rows().

    4. Возврат клавиатуры

      • Возвращается InlineKeyboardMarkup или ReplyKeyboardMarkup.

  3. Динамическая клавиатура:

    1. Вызывается create_dynamic_keyboard(...)

      • Получает:

        • buttons_dict (с кнопками);

        • lang, keyboard_type;

        • ключ для хранения key_in_storage;

        • заголовок key_header_title;

        • необязательное дополнительное действие (например, Назад).

    2. Подготовка структуры кнопок

      • Метод _getprepare_data_to_buttons_dict():

        • добавляет шапку (fixed_top), действия (fixed_bottom), основное тело (buttons);

        • сохраняет в BUTTONS_KEYBOARD_STORAGE.

    3. Обработка пагинации и разбивка на страницы

      • Метод _getprepare_data_to_keyboard_data():

        • вызывает create_buttons() с dynamic режимом;

        • делит на страницы с помощью paginatebuttons() и _groupbuttons_for_layout().

    4. Построение финальной клавиатуры

      • Метод _buildkeyboard_rows() собирает:

        • шапку;

        • текущие кнопки;

        • кнопки пагинации (если нужно);

        • нижние действия (например, Назад).

    5. Возврат клавиатуры

      • Возвращает InlineKeyboardMarkup или ReplyKeyboardMarkup.

Отдельно стоит упомянуть одну из ключевых архитектурных ошибок — мы решили указывать в поле data каждой кнопки полный callback, text или url. На первый взгляд — просто, понятно, прозрачно. На практике — оказалось совсем не так.

Позже мы заменили это на классы Callback’ов, и это решение принесло гораздо больше гибкости и порядка. Почему мы к этому пришли? Всё стало ясно, когда нам понадобилось изменить структуру проекта и перенести “Отслеживаемые типы событий” из раздела “Проект” в “Экземпляры проекта”. Казалось бы, мелочь, но тогда стало очевидно, насколько неудобно и хрупко было всё построено.

К тому же, Telegram ограничивает длину callback’а 64 символами. И даже при относительно простой иерархии меню мы быстро врезались в этот лимит — и ощутили всю боль.

Но почему мы вообще пошли по такому пути в первой версии? Причин было несколько:
1.Мы хотели чётко контролировать весь путь, чтобы реализовать универсальный механизм “назад на один уровень”, без хардкода маршрутов.
2.Первую версию писал Виктор — тогда он ещё не знал о всех тонкостях и подводных камнях, а идея с Callback-классами просто не приходила в голову. Хотел как лучше.
3.Ну и… клавиатура оказалась медленной. Очень медленной.

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

Но самое болезненное — это разлад внутри команды. Мы чувствовали бессилие. Формально всё работало, но ощущения были, будто таскаешь за собой железный куб вместо лёгкого конструктора. И становилось всё менее понятно: продолжать это тащить дальше или переписать с нуля?

Ознакомиться с этой версией клавиатуры можете тут.

Вариант №2

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

Клавиатура по-прежнему строится по знакомым принципам:
•Разделение конфигураций по .yaml-файлам никуда не делось — это оказалось удобно.
•Мы по-прежнему обрабатываем клавиатуры по типам: статические и динамические.
•Есть единый экземпляр класса клавиатуры, реализованный через синглтон — он управляет всем взаимодействием внутри.

Проект не переписывался с нуля, но стал проще, стройнее и — главное — устойчивее. Мы отказались от догматизма и сосредоточились на практической пользе. Именно это стало основой для следующей версии.

Структура файлов для создания клавиатур

  1. Файл с кнопками:

    get_main_menu:     text: get_main_menu     callback_class: MenuData

    Раньше мы указывали type и data, теперь объединили их в одно универсальное поле — callback_class. Такая схема проще и выразительнее: за всю логику теперь отвечают Callback-классы, а не текстовые коллбеки, набитые руками.

    Что это дало:

    • Мы полностью ушли от хардкода callback’ов;

    • Роутеры стали проще: не нужно больше «парсить» строки и вытаскивать из них суть;

    • Код стал понятнее и безопаснее — меньше шансов ошибиться в одном символе и получить неожиданный результат;

    • Мы упростили фильтрацию в роутерах, потому что нам не нужно «парсить» и обрабатывать коллбек.

  2. Файл с языками:

    Раньше все языки хранились в одном огромном YAML-файле. Пока у нас было 2 языка — всё шло гладко. Но стоило задуматься о масштабировании — и стало ясно: такой подход не выживет.

    Теперь у нас есть точка входа:

    ru: !include lang/ru/keyboard_text.yaml   en: !include lang/en/keyboard_text.yaml  

    Каждый язык — в своём отдельном файле. Это упростило как поддержку, так и внесение изменений. Локализации теперь можно расширять буквально одной строкой.

    В остальном, все осталось без существенных изменений.

  3. Файлы с клавиатурами:
    Ранее у нас был 1 файл со статическими клавиатурами и отдельные файлы с динамическими клавиатурами для каждого модуля. Это тоже оказалось неэффективным и поэтому мы пришли к выводу, что лучше сделать:

    • файл со статическими клавиатурами;

    • файл с динамическими клавиатурами;

    • файл с «чекбокс» клавиатурами (это наша маленькая гордость, которую придумал Виктор Королев (он один из тех, кто захотел присоединиться к разработке нашего проекта)).

Файл со статическими клавиатурами:

Мы решили также «включать» данные из сторонних .yaml файлов, чтобы:

  1. Упростить процесс взаимодействия с кнопками;

  2. Получить возможность при необходимости указывать аргументы, которые требуют конкретные callback классы;

  3. Получить возможность переопределять конкретные поля для любой из кнопок (например, мы часто использовали кнопку «Назад», но по-умолчанию ей соответствует текст «Назад», в то время как где-то уместнее использовать «Отмена» или «В меню». Или если необходимо переопределить 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 

Пример чекбокс клавиатуры:

Алгоритм работы (обобщенный)

  1. Статическая клавиатура

    1. Получение конфигурации по ключу:

      • self._static_keyboards.get(kb_key).

    2. Определение типа клавиатуры:

      • по полю keyboard_typeINLINE или REPLY.

    3. Инициализация билдера:

      • InlineKeyboardBuilder() или ReplyKeyboardBuilder().

    4. Создание кнопок:

      • await _generate_static_buttons_row(...):

        • перебирает buttons_list;

        • вызывает _get_static_inline_button или _get_static_reply_button;

        • подставляет переводы;

        • добавляет KeyboardButtonRequestUsers, если request_users=True.

    5. Опционально добавляется меню-кнопка:

      • _get_menu_button() — кнопка “В главное меню”.

    6. Формирование объекта клавиатуры:

      • builder.as_markup(...).

  2. Динамическая клавиатура

    1. Получение конфигурации по ключу из dynamic_keyboards.

    2. Инициализация InlineKeyboardBuilder.

    3. Создание заголовка (опционально):

      • _generate_keyboard_header() создаёт строку из 3 кнопок: пустая, заголовок, пустая.

    4. Создание кнопок из данных:

      • _generate_dynamic_buttons(...):

      • перебирает список data;

      • для каждой строки:

      • достаёт text_field (например, project.name);

      • собирает args в словарь;

      • создаёт InlineKeyboardButton.

    5. Добавление пагинации (если count > page_limit):

      • _get_pagination_buttons(...):

      • вычисляет общее количество страниц;

      • добавляет ⬅️ текущая страница ➡️.

    6. Добавление нижних статических кнопок:

      • также через _generate_static_buttons_row.

    7. Меню-кнопка — опционально.

    8. Финальная клавиатура: builder.as_markup().

  3. Чекбокс клавиатура

    1. Получение конфигурации по ключу из checkbox_keyboards.

    2. Создание списка чекбоксов:

      • items = list(zip(texts, ids)) — отображение текста и id;

      • для каждого:

      • определяется, выбран ли item;

      • формируется текст с ✅ или ⬜;

      • создаётся callback_data на CheckboxData.

    3. Добавляется кнопка подтверждения (OK):

      • с action="confirm" и текущими selected_ids.

    4. Добавляются нижние кнопки (если указаны).

    5. Возврат 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. Руководство отдела или какие-нибудь СЕО (топ-менеджемент) хотят быть в курсе событий, но залезать каждый раз на доску и отслеживать изменения им может быть не удобно. В таком случае под них ПМ создает отдельный экземпляр и указывает, что для них нужно отслеживать обновления эпиков, чтобы не пропустить самое главное;

  2. Ребятам, занимающимся дизайном совершенно не интересно какие решения придумали бэкенд разработчики и получать уведомления о их достижениях они тоже не хотят. Для этого ПМ создает отдельный экземпляр и указывает, что для них нужно отслеживать конкретный спринт или юзер-стори;

К сожалению, пока что реализован только 1-й сценарий, поскольку для второго нам не хватает информации о том, насколько это удобно и нужно реальному пользователю. На основе такой информации мы бы смогли сделать такой функционал, который и правда будет полезен, а не просто функционал ради функционала.

Внешний вид меню:

Мы стремились сделать интерфейс максимально простым и понятным, насколько это вообще возможно при работе с ботами. Главное правило — не проваливаться во вложенные меню глубже, чем Алиса в Страну чудес.

Особенно это касается динамических клавиатур. Именно там проще всего скатиться в хаос и нагромождение уровней. Чтобы этого избежать, мы выработали несколько простых и чётких принципов:

  1. Всегда есть заголовок. Он идёт первым и помогает пользователю понять, где он находится.

  2. Показываем ровно 5 сгенерированных кнопок. Больше — уже визуальный шум, меньше — ощущение пустоты.

  3. В нижнем левом углу — кнопка “Назад”. Она возвращает на предыдущий уровень. Никаких догадок, всё предсказуемо.

  4. В правом нижнем углу — кнопка “Добавить”. В зависимости от контекста это может быть «Добавить проект», «Добавить экземпляр», «Добавить администратора» и т. д.

  5. Если записей больше 5 — добавляется строка пагинации. Без неё теряется управляемость, а с ней — пользователь всегда видит, что есть ещё страницы.

Такой подход позволил нам сохранить визуальную лёгкость, понятную структуру и избежать UX-хаоса. А главное — пользователи бота не задают вопросов вроде «где я?» и «как отсюда выйти?».

Добавление проектов

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

После успешного добавления проекта, пользователь получает выбор:

  • Перейти к редактированию — и сразу начать настраивать проект, добавлять экземпляры, администраторов и т.д.

  • Вернуться в меню — если хочет продолжить работу с другими разделами.

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

Добавление экземпляров

Чтобы добавить экземпляр, пользователь должен сначала открыть нужный проект. Это можно сделать сразу после создания проекта или позже — через пункт меню «Проекты».

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

Добавил проект → видишь кнопку «Добавить экземпляр» — и дальше всё уже знакомо.

Такая единообразная структура помогает «приучить» пользователя к интерфейсу и снять лишнюю когнитивную нагрузку.

Редактирование экземпляров

Мы прекрасно понимали: если дать пользователю возможность что-то добавить, то точно так же нужно дать ему возможность этим управлять.

Поэтому мы реализовали простое и логичное меню управления, где пользователь может:

  1. Получить ссылку для добавления в Taiga;

  2. Получить информацию об экземпляре;

  3. Изменить название экземпляра;

  4. Изменить источники для отправки;

  5. Изменить типы отслеживаемых событий;

  6. Удалить экземпляр проекта.

Добавление чатов в экземпляры

Ранее мы уже несколько раз упоминали экземпляры (инстансы) — пора подробнее рассказать, зачем они вообще нужны.

Экземпляр — это сущность, которая позволяет:

  • Определить, куда улетать уведомлениям. То есть, в какой чат или в какую тему внутри Telegram.

  • Настроить фильтрацию событий. Какие именно типы событий отслеживать, чтобы по ним формировать уведомления: задачи, баги, комментарии и так далее.

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

Добавление типов отслеживаемых событий в экземпляры

Ранее мы уже упоминали нашу маленькую гордость — чекбокс-клавиатуры, и даже показывали, как они выглядят. Но именно сейчас этот элемент начинает играть ключевую роль.

Когда пользователь создаёт экземпляр проекта, он может сразу же настроить, какие типы событий следует отслеживать. И здесь в игру вступает чекбокс-клавиатура.

В рамках одного меню пользователь:

  • Видит список всех доступных типов событий (например: создание задачи, изменение статуса, комментарии и т.п.);

  • Может включать или отключать их одним нажатием, прямо в интерфейсе — без переходов, без подтверждений, без лишнего шума.

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


«Отлов» ошибок

В любом проекте неизбежны ошибки и не все из них получается обработать сразу. Для этого в проекте есть целых два обработчика ошибок:

  1. Обработчик ошибок от FastAPI

  2. Обработчик ошибок от 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
Поддержать проект — звездочкой, фидбэком или идеей 🙌

Нам также было бы приятно, если бы вы положительно оценили эту статью и участвовали в обсуждении.

До встречи в следующих статьях!


ссылка на оригинал статьи https://habr.com/ru/articles/897118/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *