
Привет, я Максим Королев из Петрович-ТЕХ, занимаюсь уровнем сервиса. Моя роль — в том числе про “как хороший сервис показать пользователю, чтобы тот не отказался от нового/сложного/страшного инструмента”.
Было так: не все пользователи ходили в техническую поддержку по “правильному” маршруту, через портал. Из-за этого мы теряли случаи, составляющие общую картину проблем, и озадачились: как упростить пользователям заход в поддержку?
Решили сделать семейство Telegram-ботов, которые сократили время на создание повторяющихся задач на 50–70%, подняли заполняемость до 100%, ускорили реакцию на инциденты.
Если в новом году хотите запилить много микроавтоматизаций через боты, наш кейс к вашему столу: вот как вышло у Петрович-ТЕХа.
Персонификация: заботимся о качестве бота правильной мотивацией разработки
Первого бота зовут Славик. Когда мы начали его создавать, написание кода, проверка скриптов и работа над ошибками были абстрактными. А потом мы придумали ему лицо — портрет осьминога, эдакого маскота функции. На обложке статьи как раз он.
Получилось, что теперь код мы пишем для Славика. Если допустить ошибку — страдает он, конкретный осьминог, а не просто «бот номер один». Это изменило нашу ответственность. Код стал качественнее, тесты серьёзнее, обработка ошибок внимательнее.
Решили дать имена и остальным ботам. Когда у каждого есть история и лицо — разработка становится созданием чего-то живого. Чуть алхимии реально помогает делать лучший код.

Архитектура и общие принципы
Все наши боты построены на общих принципах, которые дают быстро развить функционал под запрос и поддерживать код в актуальном состоянии.
Слоистая архитектура
Мы используем подход, близкий к Clean Architecture, с разделением на слои:
Transport Layer (Telegram Bot API)
Обработчики команд и сообщений
FSM для управления диалогами
Валидация входных данных
Application Layer (Use Cases)
Бизнес-логика создания задач
Обработка сценариев (регистрация, создание заявки)
Координация между слоями
Domain Layer (Бизнес-сущности)
Модели данных (User, Ticket, Failure)
Бизнес-правила и валидация
Value Objects (Priority, Status)
Infrastructure Layer (Внешние интеграции)
JIRA API клиент
Хранилища данных (JSON, PostgreSQL)
Службы уведомлений
Такое разделение позволяет легко менять детали реализации без влияния на бизнес-логику.
FSM для управления диалогами
Все многошаговые сценарии (регистрация, создание заявки) реализованы через Finite State Machine (FSM) из aiogram. Это чтобы четко контролировать последовательность шагов, сохранять промежуточные данные, легко добавлять новые шаги или изменять их порядок и обрабатывать отмену на любом этапе.
Пример структуры состояний для создания заявки:
class SearchFormStates(StatesGroup):
SELECT_PROBLEMATIC_SERVICE = State() # Шаг 1: Выбор сервиса
SELECT_REQUEST_TYPE = State() # Шаг 2: Тип запроса
ENTER_CITY = State() # Шаг 3: Город
ENTER_COMMENT = State() # Шаг 4: Комментарий
UPLOAD_ATTACHMENT = State() # Шаг 5: Файл
CONFIRM_AND_CREATE = State() # Шаг 6: ПодтверждениеХранение состояния
Для небольших ботов (до 100 активных пользователей) используем JSON-файлы с thread-safe блокировками (RLock). Это просто и достаточно быстро.
Для масштабирования переходим на:
In-memory кэш с TTL для часто запрашиваемых данных
PostgreSQL для персистентного хранения
Redis для распределенных блокировок (если нужно)
Обёртка над JIRA REST API
Все боты используют единый модуль для работы с JIRA. Это даёт:
Единообразную обработку ошибок
Retry-механизм с экспоненциальной задержкой
Кэширование метаданных (типы задач, поля, transitions)
Логирование всех запросов
Пример: метод создания задачи из общего модуля JIRA. Показывает, как обрабатываем ошибки, логируем запросы и делаем retry.
@handle_jira_errors
async def create_issue(self, issue_data: dict) -> JiraIssue:
"""Создает новую задачу в JIRA с retry и логированием"""
if not self.session:
raise JiraConnectionError("Сессия не инициализирована")
# Логируем запрос для отладки
logger.info(f"Создание задачи: {issue_data.get('fields', {}).get('summary')}")
max_retries = 3
for attempt in range(max_retries):
try:
async with self.session.post(
f"{self.base_url}/issue",
json=issue_data,
timeout=self.config.REQUEST_TIMEOUT
) as response:
# Проверяем статус ответа
if response.status == 200:
data = await response.json()
logger.info(f"Задача создана: {data.get('key')}")
return data
elif response.status == 429: # Rate limit
# Экспоненциальная задержка
wait_time = (2 ** attempt) * 2
logger.warning(f"Rate limit, ждём {wait_time} сек")
await asyncio.sleep(wait_time)
continue
else:
# Другие ошибки — пробрасываем дальше
text = await response.text()
raise JiraError(f"HTTP {response.status}: {text}")
except asyncio.TimeoutError:
if attempt < max_retries - 1:
logger.warning(f"Timeout, попытка {attempt + 1}/{max_retries}")
await asyncio.sleep(2 ** attempt)
continue
raise JiraConnectionError("Превышено время ожидания ответа от JIRA")
raise JiraError("Не удалось создать задачу после всех попыток")• Декоратор @handle_jira_errors централизует обработку ошибок
• Retry только для временных ошибок (429, timeout)
• Экспоненциальная задержка не перегружает JIRA
• Логирование помогает отлаживать проблемы в production
Безопасность
Все чувствительные данные в .env файлах
Токены никогда не коммитятся в Git
Используем python-dotenv для загрузки переменных окружения
Whitelist пользователей
Система регистрации с модерацией
Роли и права доступа
Rate limiting для защиты от спама (3 команды/сек)
Все входные данные проверяются перед обработкой
Великолепная шестерка ботов
Теперь сами боты. Опишу их по схеме: задача — функция бота — из чего бот собран — пример кода — лайфхаки реализации — результаты.
"Лупа" — репортинг ошибок поиска в JIRA

Задача: сотруднику нужно быстро зафиксировать баг в поиске на сайте или приложении — не отвлекаясь от работы.
Что делает:
Интерактивная форма создания задачи — выбор сервиса, тип проблемы, город
Обязательная загрузка файла (фото/видео, до 10 МБ)
Автоматическое заполнение всех полей в JIRA
Логирование в Excel для статистики
Технически:
aiogram 3.x с FSM (Finite State Machine)
Асинхронная архитектура
Кэширование подразделений из JIRA
Валидация номеров и защита от дублей
Пример: это обработчик шага выбора сервиса в "Лупе". Показывает, как валидируем ввод, сохраняем данные и переходим к следующему шагу.
@router.callback_query(F.data.startswith("service_"))
async def process_service_selection(
callback: CallbackQuery,
state: FSMContext
):
"""Обработка выбора проблемного сервиса"""
user_id = callback.from_user.id
service = callback.data.replace("service_", "")
# Сохраняем выбранный сервис в состоянии
await state.update_data(problematic_service=service)
# Переходим к следующему шагу
await state.set_state(SearchFormStates.SELECT_REQUEST_TYPE)
# Показываем клавиатуру с типами запросов
await callback.message.edit_text(
"Выберите тип запроса:",
reply_markup=create_request_type_keyboard()
)
await callback.answer()• Используем callback_query для кнопок — это надёжнее, чем текстовый ввод
• Сохраняем данные в state перед переходом — если пользователь отменит, данные не потеряются
• Используем edit_text вместо нового сообщения — меньше спама в чате
Результаты:
30+ задач за первый месяц
Время создания задачи: 5-10 мин → 2-3 мин (-60%)
100% корректное заполнение полей
"Рафтар" — сбор статусов проектов

Задача: собрать ежемесячный статус для контроля жизненного цикла 50+ проектов.
Что делает:
Пошаговый мастер заполнения
Блокировка повторного заполнения в течение месяца
Автоматическое создание RAF-задач в JIRA
Система напоминаний: ежемесячная рассылка + ежедневные пуш-напоминания + финальное уведомление
Excel-отчеты
Технически:
Планировщик задач (scheduler) для автоматических напоминаний
Умный поиск transitions в JIRA
Система отслеживания напоминаний (JSON)
Пример: это фрагмент планировщика из "Рафтара". Показывает, как формируется задание “напоминание” и как храним состояние.
class StatusReminderScheduler:
"""Планировщик для автоматических напоминаний"""
def __init__(self, bot: Bot):
self.bot = bot
self.is_running = False
async def send_monthly_reminders(self):
"""Отправка месячных напоминаний в первый рабочий день"""
period = get_current_period() # "Октябрь 2024"
# Загружаем базу напоминаний
db = load_reminders_db()
# Получаем всех пользователей с активными проектами
all_projects = get_all_projects()
users_with_projects = set()
for project in all_projects.values():
users_with_projects.update(project.get("users", []))
sent_count = 0
for user_id in users_with_projects:
# Проверяем, не заполнены ли уже статусы
if check_status_exists(user_id, period):
continue
# Отправляем напоминание
try:
await self.bot.send_message(
user_id,
f" 