Cat

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

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

Taigram (Taiga Telegram Notifier) proDream 03 Апрель 2025 Просмотров: 51

Продолжаем рассказывать о разработке нашего 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()

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

Наша идея

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

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

  1. Поддержка мультиязычности - с учетом того, что проект OpenSource и при расширении локализации должна быть возможность удобно добавить поддержку новых языков;
  2. Удобство при редактировании текста кнопок и структуры клавиатур;
  3. Возможность переиспользования кнопок в разных клавиатурах и разных сценариях;
  4. Создание как статических (заранее предопределенных), так и динамических клавиатур (содержимое которых нам заранее не известно);
  5. Поддержка пагинации;
  6. Удобство для дальнейшей разработки.

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

Вариант №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 строка кода. Хотя стоит уточнить: это вместе с тайпхинтами и докстрингами.

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

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

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

  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) через _group_buttons_into_fixed_rows().
    4. Возврат клавиатуры
      • Возвращается InlineKeyboardMarkup или ReplyKeyboardMarkup.
  3. Динамическая клавиатура:
    1. Вызывается create_dynamic_keyboard(...)
      • Получает:
      buttons_dict (с кнопками);
      lang, keyboard_type;
      • ключ для хранения key_in_storage;
      • заголовок key_header_title;
      • необязательное дополнительное действие (например, Назад).
    2. Подготовка структуры кнопок
      • Метод _get_prepare_data_to_buttons_dict():
      • добавляет шапку (fixed_top), действия (fixed_bottom), основное тело (buttons);
      • сохраняет в BUTTONS_KEYBOARD_STORAGE.
    3. Обработка пагинации и разбивка на страницы
      • Метод_get_prepare_data_to_keyboard_data():
      • вызывает create_buttons() с dynamic режимом;
      • делит на страницы с помощью _paginate_buttons() и _group_buttons_for_layout().
    4. Построение финальной клавиатуры
      • Метод _build_keyboard_rows() собирает:
      • шапку;
      • текущие кнопки;
      • кнопки пагинации (если нужно);
      • нижние действия (например, Назад).
    5. Возврат клавиатуры
      • Возвращает 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 файлов, чтобы:

  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: 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-й сценарий, поскольку для второго нам не хватает информации о том, насколько это удобно и нужно реальному пользователю. На основе такой информации мы бы смогли сделать такой функционал, который и правда будет полезен, а не просто функционал ради функционала.

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

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

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

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

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

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

Автор

    Нет комментариев

    Реклама