feat: кнопка «Ответить» на сообщении оператора
Оператор мог ответить, когда пользователь уже вышел из режима поддержки.
Сообщение приходило, но ответить было нечем: снова идти в «Помощь →
Поддержка», а про прежний контекст уже ничего не напоминало.
- К сообщению оператора добавляется инлайн-кнопка «↩️ Ответить», только
если обращение закрыто (при активном пользователь и так пишет оператору).
- Кнопка ставит 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
This commit is contained in:
1 parent
175d618d62
commit
21f4adf0b1
5 files changed
+178
-7
No files matched your search
+89
-6
@@ -16,10 +16,24 @@ from contextlib import closing
|
||||
from typing import Any, Awaitable, Callable
|
||||
|
||||
from aiogram import BaseMiddleware, Bot, F, Router
|
||||
from aiogram.types import Message, TelegramObject, User
|
||||
from aiogram.types import (
|
||||
CallbackQuery,
|
||||
InlineKeyboardButton,
|
||||
InlineKeyboardMarkup,
|
||||
Message,
|
||||
TelegramObject,
|
||||
User,
|
||||
)
|
||||
|
||||
from activity_phrases import support_greeting
|
||||
from keyboards import MAIN_MENU_VALUE, SUPPORT_BUTTON, SUPPORT_VALUE, TRAINING_RESET_VALUE, resolve_button_text
|
||||
from keyboards import (
|
||||
MAIN_MENU_VALUE,
|
||||
SUPPORT_BUTTON,
|
||||
SUPPORT_VALUE,
|
||||
TRAINING_RESET_VALUE,
|
||||
mode_keyboard,
|
||||
resolve_button_text,
|
||||
)
|
||||
from user_store import connect, support_profile_summary
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -67,6 +81,27 @@ def note_prefix() -> str:
|
||||
DEFAULT_OPERATOR_SIGNATURE = "Оператор поддержки"
|
||||
|
||||
|
||||
REPLY_TO_OPERATOR_BUTTON = "↩️ Ответить"
|
||||
SUPPORT_REPLY_CALLBACK_PREFIX = "support:reply:"
|
||||
SUPPORT_REPLY_PROMPT = "Жду ваше сообщение для оператора."
|
||||
|
||||
|
||||
def support_reply_callback_data(user_id: int) -> str:
|
||||
return f"{SUPPORT_REPLY_CALLBACK_PREFIX}{user_id}"
|
||||
|
||||
|
||||
def support_reply_keyboard(user_id: int) -> InlineKeyboardMarkup:
|
||||
"""Кнопка «Ответить» под сообщением оператора, если обращение закрыто."""
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[[
|
||||
InlineKeyboardButton(
|
||||
text=REPLY_TO_OPERATOR_BUTTON,
|
||||
callback_data=support_reply_callback_data(user_id),
|
||||
)
|
||||
]]
|
||||
)
|
||||
|
||||
|
||||
def operator_signature() -> str:
|
||||
"""Подпись, с которой ответ оператора приходит пользователю в личку.
|
||||
|
||||
@@ -435,19 +470,33 @@ async def _deliver_operator_reply(bot: Bot, message: Message, user_id: int) -> N
|
||||
|
||||
Текст уходит одним сообщением с подписью в начале. Медиа копируется
|
||||
`copy_message` (caption у копии задать нельзя), поэтому подпись отправляется
|
||||
отдельным сообщением перед ним.
|
||||
отдельным сообщением перед ним. Если обращение закрыто, к сообщению
|
||||
добавляется кнопка «Ответить» — она возвращает в режим общения с поддержкой.
|
||||
"""
|
||||
signature = operator_signature()
|
||||
# Кнопка «Ответить» нужна только если пользователь вне режима поддержки:
|
||||
# иначе он и так пишет оператору, и кнопка была бы лишней.
|
||||
markup = None
|
||||
try:
|
||||
if not await asyncio.to_thread(is_support_active, user_id):
|
||||
markup = support_reply_keyboard(user_id)
|
||||
except Exception as exc:
|
||||
logger.warning("Support active lookup failed (%s)", type(exc).__name__)
|
||||
|
||||
text = (message.text or "").strip()
|
||||
if text:
|
||||
await bot.send_message(chat_id=user_id, text=f"{signature}:\n{text}")
|
||||
await bot.send_message(
|
||||
chat_id=user_id, text=f"{signature}:\n{text}", reply_markup=markup
|
||||
)
|
||||
return
|
||||
caption = (message.caption or "").strip()
|
||||
if caption:
|
||||
# У медиа есть подпись — пересылаем её текстом, само медиа копируем ниже.
|
||||
await bot.send_message(chat_id=user_id, text=f"{signature}:\n{caption}")
|
||||
await bot.send_message(
|
||||
chat_id=user_id, text=f"{signature}:\n{caption}", reply_markup=markup
|
||||
)
|
||||
else:
|
||||
await bot.send_message(chat_id=user_id, text=f"{signature}:")
|
||||
await bot.send_message(chat_id=user_id, text=f"{signature}:", reply_markup=markup)
|
||||
await bot.copy_message(
|
||||
chat_id=user_id,
|
||||
from_chat_id=message.chat.id,
|
||||
@@ -461,6 +510,40 @@ def _is_service_message(message: Message) -> bool:
|
||||
return bool(prefix) and text.startswith(prefix)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith(SUPPORT_REPLY_CALLBACK_PREFIX))
|
||||
async def handle_support_reply(callback: CallbackQuery) -> None:
|
||||
"""Кнопка «Ответить»: возвращает пользователя в режим общения с поддержкой."""
|
||||
raw = (callback.data or "").split(":", 2)[-1]
|
||||
try:
|
||||
owner_id = int(raw)
|
||||
except ValueError:
|
||||
await callback.answer("Некорректная кнопка.", show_alert=True)
|
||||
return
|
||||
if callback.from_user is None or callback.from_user.id != owner_id:
|
||||
await callback.answer("Эта кнопка предназначена другому пользователю.", show_alert=True)
|
||||
return
|
||||
if not is_enabled():
|
||||
await callback.answer(SUPPORT_UNAVAILABLE_TEXT, show_alert=True)
|
||||
return
|
||||
try:
|
||||
await asyncio.to_thread(set_active, owner_id, True)
|
||||
except Exception as exc:
|
||||
logger.error("Support reactivate failed user=%s (%s)", owner_id, type(exc).__name__)
|
||||
await callback.answer("Не удалось открыть обращение. Попробуй ещё раз.", show_alert=True)
|
||||
return
|
||||
await callback.answer()
|
||||
message = callback.message
|
||||
if isinstance(message, Message):
|
||||
try:
|
||||
await message.edit_reply_markup(reply_markup=None)
|
||||
except Exception:
|
||||
logger.debug("Support reply button removal failed")
|
||||
await message.answer(
|
||||
SUPPORT_REPLY_PROMPT,
|
||||
reply_markup=mode_keyboard("help", support=True),
|
||||
)
|
||||
|
||||
|
||||
@router.message(
|
||||
F.chat.func(_is_support_chat),
|
||||
F.is_topic_message.as_("topic"),
|
||||
|
||||
Reference in new issue
Block a user