feat: подпись имени пользователя на сообщении в топике
В топике было непонятно, от кого сообщение: текст приходил без автора. - Текст уходит в топик как «<Имя>:\n<текст>»; имени нет — «<id>:\n<текст>» (user_signature()). - Caption медиа уходит текстом с той же подписью, само медиа копируется. - Всё остальное (фото, документы, голосовые без подписи) копируется как есть, без подписи — как и просили «если не текст, то пофиг». Симметрично подписи оператора «Оператор поддержки:» на ответах. Тесты: 244 OK (8 новых — подпись имени, fallback на id, медиа с caption, медиа без текста копируется без подписи). VERSION: 0.1.0+57 -> 0.1.0+58
This commit is contained in:
1 parent
6938c1d0f3
commit
175d618d62
5 files changed
+130
-7
No files matched your search
@@ -148,6 +148,8 @@ Telegram-бот, который по запросу пользователя г
|
||||
(`activity_phrases.support_greeting()`): «Здравствуйте! На связи поддержка. Расскажите,
|
||||
что случилось — разберёмся.» Формулировки безличные, без «я / бот / оператор».
|
||||
Пул переопределяется переменной `SUPPORT_GREETINGS` (разделитель `|`) без деплоя.
|
||||
- **Подпись пользователя**: в топик сообщение уходит как «<Имя>:\n<текст>»; если имени нет —
|
||||
«<id>:\n<текст>». Caption медиа уходит текстом с подписью, само медиа — как есть.
|
||||
- **Подпись ответа**: пользователю сообщение приходит как «Оператор поддержки:\n<текст>».
|
||||
Переменная `SUPPORT_OPERATOR_SIGNATURE` (default «Оператор поддержки»). У медиа caption не
|
||||
подменяется, поэтому подпись уходит отдельным сообщением перед копией медиа.
|
||||
|
||||
@@ -95,6 +95,9 @@ onboarding. В «Подписке» сразу отображаются тип
|
||||
Порядок включения роутеров критичен — у aiogram побеждает первый подходящий.
|
||||
Планируемые роли: **работник** (личка), **помощник поддержки** (группа),
|
||||
**аналитик** (сбор аналитики), **почтальон** (рассылки).
|
||||
- **Сообщение пользователя в топике подписывается**: «<Имя>:\n<текст>», а если имени нет —
|
||||
«<id>:\n<текст>» (`user_signature()`). Caption медиа тоже уходит текстом с подписью,
|
||||
само медиа копируется как есть.
|
||||
- Первое сообщение — приветствие оператора. Бот не объясняет, что «передаёт» сообщение:
|
||||
это разрушает ощущение живого диалога. Пользователь видит случайную фразу из пула
|
||||
`SUPPORT_GREETINGS` (`activity_phrases.support_greeting()`), например
|
||||
|
||||
+40
-6
@@ -331,6 +331,45 @@ def _unavailable_text(reason: str | None = None) -> str:
|
||||
)
|
||||
|
||||
|
||||
def user_signature(user: User) -> str:
|
||||
"""Подпись сообщения пользователя в топике: имя или id, если имени нет."""
|
||||
full_name = " ".join(
|
||||
part for part in (user.first_name, user.last_name) if part
|
||||
).strip()
|
||||
return full_name or str(user.id)
|
||||
|
||||
|
||||
async def _send_user_message_to_topic(
|
||||
bot: Bot, message: Message, group_id: int, thread_id: int, user: User
|
||||
) -> None:
|
||||
"""Отправляет сообщение пользователя в топик, добавляя подпись к тексту.
|
||||
|
||||
Текст уходит одним сообщением «<Имя>:\n<текст>». Всё остальное (фото,
|
||||
документы, голосовые) копируется как есть, без подписи.
|
||||
"""
|
||||
text = (message.text or "").strip()
|
||||
if text:
|
||||
await bot.send_message(
|
||||
chat_id=group_id,
|
||||
text=f"{user_signature(user)}:\n{text}",
|
||||
message_thread_id=thread_id,
|
||||
)
|
||||
return
|
||||
caption = (message.caption or "").strip()
|
||||
if caption:
|
||||
await bot.send_message(
|
||||
chat_id=group_id,
|
||||
text=f"{user_signature(user)}:\n{caption}",
|
||||
message_thread_id=thread_id,
|
||||
)
|
||||
await bot.copy_message(
|
||||
chat_id=group_id,
|
||||
from_chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
message_thread_id=thread_id,
|
||||
)
|
||||
|
||||
|
||||
async def relay_user_message(message: Message) -> bool:
|
||||
"""Копирует сообщение пользователя в его топик. True — сообщение ушло."""
|
||||
bot = message.bot
|
||||
@@ -346,12 +385,7 @@ async def relay_user_message(message: Message) -> bool:
|
||||
logger.debug("Support unavailable notice failed")
|
||||
return False
|
||||
try:
|
||||
await bot.copy_message(
|
||||
chat_id=group_id,
|
||||
from_chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
message_thread_id=thread_id,
|
||||
)
|
||||
await _send_user_message_to_topic(bot, message, group_id, thread_id, user)
|
||||
return True
|
||||
except Exception as exc:
|
||||
if _is_missing_thread(exc):
|
||||
|
||||
@@ -345,6 +345,90 @@ class SupportStartTextTests(unittest.TestCase):
|
||||
self.assertNotIn("оператор", rendered.lower())
|
||||
|
||||
|
||||
class UserSignatureTests(unittest.IsolatedAsyncioTestCase):
|
||||
"""Сообщение пользователя в топике подписывается его именем или id."""
|
||||
|
||||
def _user(self, first_name="Иван", last_name=None, user_id=42):
|
||||
return SimpleNamespace(
|
||||
id=user_id, first_name=first_name, last_name=last_name, is_bot=False
|
||||
)
|
||||
|
||||
def test_signature_uses_first_and_last_name(self):
|
||||
self.assertEqual(
|
||||
support.user_signature(self._user("Иван", "Петров")), "Иван Петров"
|
||||
)
|
||||
|
||||
def test_signature_uses_first_name_only(self):
|
||||
self.assertEqual(support.user_signature(self._user("Иван")), "Иван")
|
||||
|
||||
def test_signature_falls_back_to_id(self):
|
||||
self.assertEqual(support.user_signature(self._user(None, None, 42)), "42")
|
||||
|
||||
def test_signature_ignores_blank_name(self):
|
||||
self.assertEqual(support.user_signature(self._user(" ", None, 42)), "42")
|
||||
|
||||
|
||||
class UserMessageToTopicTests(unittest.IsolatedAsyncioTestCase):
|
||||
"""Сообщение пользователя уходит в топик с подписью; медиа — как есть."""
|
||||
|
||||
def _user(self, first_name="Иван", last_name=None, user_id=42):
|
||||
return SimpleNamespace(
|
||||
id=user_id, first_name=first_name, last_name=last_name, is_bot=False
|
||||
)
|
||||
|
||||
async def test_text_is_sent_with_signature(self):
|
||||
bot = AsyncMock()
|
||||
message = SimpleNamespace(
|
||||
text="не работает оплата", caption=None,
|
||||
chat=SimpleNamespace(id=555), message_id=10,
|
||||
)
|
||||
await support._send_user_message_to_topic(bot, message, -100123, 14, self._user())
|
||||
|
||||
bot.send_message.assert_awaited_once()
|
||||
kwargs = bot.send_message.await_args.kwargs
|
||||
self.assertEqual(kwargs["chat_id"], -100123)
|
||||
self.assertEqual(kwargs["message_thread_id"], 14)
|
||||
self.assertEqual(kwargs["text"], "Иван:\nне работает оплата")
|
||||
bot.copy_message.assert_not_awaited()
|
||||
|
||||
async def test_media_without_text_is_copied_as_is(self):
|
||||
bot = AsyncMock()
|
||||
message = SimpleNamespace(
|
||||
text=None, caption=None,
|
||||
chat=SimpleNamespace(id=555), message_id=11,
|
||||
)
|
||||
await support._send_user_message_to_topic(bot, message, -100123, 14, self._user())
|
||||
|
||||
bot.send_message.assert_not_awaited()
|
||||
bot.copy_message.assert_awaited_once_with(
|
||||
chat_id=-100123, from_chat_id=555, message_id=11, message_thread_id=14
|
||||
)
|
||||
|
||||
async def test_caption_is_sent_as_text_then_media_copied(self):
|
||||
bot = AsyncMock()
|
||||
message = SimpleNamespace(
|
||||
text=None, caption="вот скриншот",
|
||||
chat=SimpleNamespace(id=555), message_id=12,
|
||||
)
|
||||
await support._send_user_message_to_topic(bot, message, -100123, 14, self._user())
|
||||
|
||||
self.assertEqual(bot.send_message.await_args.kwargs["text"], "Иван:\nвот скриншот")
|
||||
bot.copy_message.assert_awaited_once_with(
|
||||
chat_id=-100123, from_chat_id=555, message_id=12, message_thread_id=14
|
||||
)
|
||||
|
||||
async def test_signature_falls_back_to_user_id(self):
|
||||
bot = AsyncMock()
|
||||
message = SimpleNamespace(
|
||||
text="привет", caption=None,
|
||||
chat=SimpleNamespace(id=555), message_id=13,
|
||||
)
|
||||
await support._send_user_message_to_topic(
|
||||
bot, message, -100123, 14, self._user(None, None, 99)
|
||||
)
|
||||
self.assertEqual(bot.send_message.await_args.kwargs["text"], "99:\nпривет")
|
||||
|
||||
|
||||
class OperatorSignatureTests(unittest.IsolatedAsyncioTestCase):
|
||||
"""Ответ оператора приходит пользователю с подписью «Оператор поддержки:»."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user