Оператор мог ответить, когда пользователь уже вышел из режима поддержки.
Сообщение приходило, но ответить было нечем: снова идти в «Помощь →
Поддержка», а про прежний контекст уже ничего не напоминало.
- К сообщению оператора добавляется инлайн-кнопка «↩️ Ответить», только
если обращение закрыто (при активном пользователь и так пишет оператору).
- Кнопка ставит is_active=1, убирает себя с сообщения и показывает
клавиатуру поддержки (только «Главное меню»).
- Callback проверяет владельца: support:reply:<user_id> сработает только
для того пользователя, кому пришло сообщение.
- Роутер support.router не затронут фильтром основного роутера
(F.message.chat.type == "private") — он на него не распространяется.
Тесты: 251 OK (7 новых). Проверено, что aiogram вызывает bot(SendMessage(...)),
а не bot.send_message — asserts адаптированы, иначе тест проходил вхолостую.
VERSION: 0.1.0+58 -> 0.1.0+59
В топике было непонятно, от кого сообщение: текст приходил без автора.
- Текст уходит в топик как «<Имя>:\n<текст>»; имени нет — «<id>:\n<текст>»
(user_signature()).
- Caption медиа уходит текстом с той же подписью, само медиа копируется.
- Всё остальное (фото, документы, голосовые без подписи) копируется как есть,
без подписи — как и просили «если не текст, то пофиг».
Симметрично подписи оператора «Оператор поддержки:» на ответах.
Тесты: 244 OK (8 новых — подпись имени, fallback на id, медиа с caption,
медиа без текста копируется без подписи).
VERSION: 0.1.0+57 -> 0.1.0+58
Ответ из топика приходил пользователю без подписи, и было непонятно, кто
пишет — бот или человек.
- Текст уходит одним сообщением: «Оператор поддержки:\n<текст>».
- У медиа copy_message не позволяет подменить caption, поэтому подпись
отправляется отдельным сообщением перед копией; caption медиа, если был,
попадает в то же сообщение.
- Подпись задаётся SUPPORT_OPERATOR_SIGNATURE (default «Оператор поддержки»).
- Заметки с префиксом // по-прежнему не уходят пользователю.
Тесты: 236 OK (6 новых — текст, медиа с caption, медиа без caption,
переопределение подписи через env).
VERSION: 0.1.0+56 -> 0.1.0+57
Сообщение оператора в топике поддержки не доходило до пользователя: бот
отвечал прямо в группу «Выбери режим работы кнопкой ниже». Причина — порядок
регистрации роутеров: основной router с catch-all @router.message() (без
фильтра по чату) включался ПЕРВЫМ, а support.router — вторым. У aiogram
побеждает первый подходящий, поэтому до поддержки очередь не доходила.
Комментарий в main() утверждал обратное.
- support.router регистрируется до основного router.
- Основной роутер отфильтрован на F.chat.type == "private" (роль «работник»);
групповые сценарии обслуживаются своими роутерами. Это первый шаг к ролям:
работник / помощник поддержки / аналитик / почтальон.
- Роутер поддержки дополнительно отсекает сообщения от ботов.
Вторая поломка того же корня: при активном обращении «📖 Помощь» и
«🎓 Обучение» уезжали оператору как обычный текст, режим не переключался и
бот в личке молчал — пользователь был заперт в обращении. Middleware теперь
пропускает все команды и все кнопки reply-клавиатуры через _is_bot_control(),
а не только «Поддержка» и «Главное меню».
Тесты: 224 OK (10 новых). Регрессия проверена: на старой логике «Помощь»
уходит оператору (relay=1), на новой обрабатывается ботом (relay=0).
Заглушки заменены на настоящие Message — middleware проверяет
isinstance(event, Message), поэтому SimpleNamespace проходил вхолостую.
is_support_active вызывается через to_thread — AsyncMock-заглушка возвращала
truthy-корутину и делала проверку фиктивной.
VERSION: 0.1.0+53 -> 0.1.0+54
Создание топика падало с «not enough rights to create a topic» (бот был
обычным участником группы, а не администратором), но пользователь видел
только «Поддержка временно недоступна. Попробуй позже» — по такому сообщению
невозможно понять, что чинить.
- ensure_topic возвращает (thread_id, причина) вместо одного thread_id.
- Текст Telegram логируется на ERROR: по типу исключения причина нечитаема.
- В DEPLOY_ENV=development причина добавляется в сообщение пользователю;
в production остаётся общая заглушка. HTML экранируется.
Проверено: 214 тестов OK (7 новых на причину и её выдачу).
Живая проверка на dev: после выдачи боту прав администратора с «Управление
темами» create_forum_topic создал топик (thread_id 18, пробный удалён).
VERSION: 0.1.0+52 -> 0.1.0+53
Обращение открывается кнопкой «🆘 Поддержка» или /support в режиме «Помощь».
Отдельного FSM-режима нет: пока обращение активно, внизу остаётся только
«Главное меню», а переключение режимов отклоняется.
- support.py: личка пользователя ↔ персональный топик группы SUPPORT_GROUP_ID.
Топик создаётся один раз на пользователя и переиспользуется, поэтому
повторные обращения продолжают ту же переписку. Пересылаются все типы
контента через copy_message; ответ оператора из топика уходит в личку;
сообщение с префиксом // остаётся внутренней заметкой. Функция выключена
без SUPPORT_GROUP_ID.
- Карточка профиля уходит первым сообщением топика: аватарка, имя, username,
id и сводка — сколько дней пользуется ботом, сколько продлений подписки,
сколько дней осталось, обучен ли бот (user_store.support_profile_summary).
- Бот не объясняет пользователю, что «передаёт оператору»: в ответ приходит
случайная фраза из пула SUPPORT_GREETINGS (activity_phrases.support_greeting).
Формулировки безличные, без «я/бот/оператор»; пул переопределяется через
SUPPORT_GREETINGS без деплоя.
Проверено: 207 тестов OK (30 новых в tests/test_support.py).
VERSION: 0.1.0+51 -> 0.1.0+52
Статус долгой операции больше не оставляет в чате отдельное сообщение:
в приватном чате он стримится живым драфтом (sendMessageDraft), пока
идет классификация, генерация, чтение скриншотов или распознавание речи.
- activity_phrases.py: пулы фраз по типу операции (generation, training,
images, voice, ideas), случайный интервал 3-8 сек (ACTIVITY_MIN_SECONDS /
ACTIVITY_MAX_SECONDS), фраза не повторяет предыдущую
- OperationStatus: драфт со стабильным draft_id, фоновая ротация фраз,
finish() отправляет финальный статус реальным сообщением, retarget()
меняет пул на лету (generate_ideas), предохранитель ACTIVITY_MAX_ROTATIONS
- в группах и топиках драфты недоступны: прежний answer + edit_text,
ротация фраз отключена; при ошибке драфта автоматический откат
- Dockerfile копирует activity_phrases.py и support.py, иначе контейнер
падает на импорте
Проверено: 172 теста OK,其中包括 14 новых в tests/test_activity_phrases.py.
Тесты нашли и закрыли баг: ротация запускалась и в группах, где каждый тик
был бы реальным редактированием сообщения.
VERSION: 0.1.0+48 -> 0.1.0+49