SilverElixir commited on
Commit
14ad7cf
·
1 Parent(s): da6a2d2

Show typing before media downloads and register localized command menus in parallel

Browse files
Files changed (3) hide show
  1. bot.py +18 -7
  2. lumen_message_core.py +10 -10
  3. tests/test_bot_state.py +20 -0
bot.py CHANGED
@@ -1611,17 +1611,28 @@ async def _webhook_startup() -> None:
1611
  }
1612
  for code, cmds in localized_commands
1613
  ]
1614
- commands_ok = False
1615
- for cmd_payload in cmd_payloads:
1616
- try:
 
 
 
 
1617
  await asyncio.wait_for(
1618
  telegram_api_call("setMyCommands", cmd_payload, request_timeout=15.0),
1619
- timeout=18.0
1620
  )
 
 
 
 
 
 
 
 
 
 
1621
  commands_ok = True
1622
- except Exception as exc:
1623
- log.warning("[webhook] setMyCommands failed (%s): %s", cmd_payload.get("language_code", "default"), exc)
1624
- # for-else без break врал бы об успехе при провале всех языков.
1625
  if commands_ok:
1626
  log.info("[webhook] Bot commands set successfully.")
1627
  else:
 
1611
  }
1612
  for code, cmds in localized_commands
1613
  ]
1614
+ # 24 локали последовательно — это 5-12 с холодного старта через прокси.
1615
+ # Регистрируем с ограниченной параллельностью: меню не влияет на ответы,
1616
+ # но не хочется упираться в flood-контроль Telegram.
1617
+ _commands_sem = asyncio.Semaphore(4)
1618
+
1619
+ async def _register_commands(cmd_payload: dict) -> None:
1620
+ async with _commands_sem:
1621
  await asyncio.wait_for(
1622
  telegram_api_call("setMyCommands", cmd_payload, request_timeout=15.0),
1623
+ timeout=18.0,
1624
  )
1625
+
1626
+ results = await asyncio.gather(
1627
+ *(_register_commands(payload) for payload in cmd_payloads),
1628
+ return_exceptions=True,
1629
+ )
1630
+ commands_ok = False
1631
+ for cmd_payload, result in zip(cmd_payloads, results):
1632
+ if isinstance(result, BaseException):
1633
+ log.warning("[webhook] setMyCommands failed (%s): %s", cmd_payload.get("language_code", "default"), result)
1634
+ else:
1635
  commands_ok = True
 
 
 
1636
  if commands_ok:
1637
  log.info("[webhook] Bot commands set successfully.")
1638
  else:
lumen_message_core.py CHANGED
@@ -658,6 +658,16 @@ async def _handle_message_core(
658
  await bot._send_pick_question(message, pick_scenario, clean_prompt)
659
  return
660
 
 
 
 
 
 
 
 
 
 
 
661
  media_too_big_mb = 0
662
  try:
663
  med_name, media_tuple = await bot._resolve_incoming_media(
@@ -718,16 +728,6 @@ async def _handle_message_core(
718
  await bot._tg_call(message.reply, bot._t(message.chat.id, "status_listening"))
719
  return
720
 
721
- # Отправка typing экшена (bot.bot — инстанс aiogram Bot, не модуль).
722
- # Не ждём ответа: это отдельный круг к Telegram через прокси, и вызов
723
- # модели стартовал бы на 0.1-0.3с позже. Индикатор косметический.
724
- if not is_guest:
725
- async def _send_typing() -> None:
726
- with contextlib.suppress(Exception):
727
- await bot.bot.send_chat_action(chat_id=message.chat.id, action="typing")
728
-
729
- bot._track_inflight_task(asyncio.create_task(_send_typing()))
730
-
731
  media_mime = media_tuple[1] if media_tuple else None
732
  # Смешанный альбом (фото + видео-слайды): достаточно одного видео — весь набор едет
733
  # в Gemini-ветку, иначе видео-слайды молча терялись (ревью ветки: смотрели только media[0]).
 
658
  await bot._send_pick_question(message, pick_scenario, clean_prompt)
659
  return
660
 
661
+ # «Печатает…» показываем сразу, ДО скачивания вложения и транскрибации:
662
+ # иначе видео на 20 МБ несколько секунд выглядело как молчание (аудит 29.10.2026).
663
+ # Не ждём ответа: круг к Telegram не должен задерживать модель.
664
+ if not is_guest:
665
+ async def _send_typing() -> None:
666
+ with contextlib.suppress(Exception):
667
+ await bot.bot.send_chat_action(chat_id=message.chat.id, action="typing")
668
+
669
+ bot._track_inflight_task(asyncio.create_task(_send_typing()))
670
+
671
  media_too_big_mb = 0
672
  try:
673
  med_name, media_tuple = await bot._resolve_incoming_media(
 
728
  await bot._tg_call(message.reply, bot._t(message.chat.id, "status_listening"))
729
  return
730
 
 
 
 
 
 
 
 
 
 
 
731
  media_mime = media_tuple[1] if media_tuple else None
732
  # Смешанный альбом (фото + видео-слайды): достаточно одного видео — весь набор едет
733
  # в Gemini-ветку, иначе видео-слайды молча терялись (ревью ветки: смотрели только media[0]).
tests/test_bot_state.py CHANGED
@@ -1680,6 +1680,26 @@ def test_message_core_sends_typing_indicator(rate_guard_setup, monkeypatch):
1680
  fake_bot.send_chat_action.assert_awaited_once_with(chat_id=123, action="typing")
1681
 
1682
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1683
  def test_message_core_does_not_wait_for_typing_before_model(rate_guard_setup, monkeypatch):
1684
  # Что защищает: вызов модели не ждёт сетевого ответа на "печатает".
1685
  # Регрессия: вернуть await перед send_chat_action — модель стартует на один
 
1680
  fake_bot.send_chat_action.assert_awaited_once_with(chat_id=123, action="typing")
1681
 
1682
 
1683
+ def test_typing_starts_before_media_download(rate_guard_setup, monkeypatch):
1684
+ # Аудит 29.10.2026: "печатает" уходил только после скачивания вложения —
1685
+ # большое видео несколько секунд выглядело как молчание.
1686
+ message = rate_guard_setup()
1687
+ message.text = "что на фото?"
1688
+ fake_bot = SimpleNamespace(send_chat_action=AsyncMock())
1689
+ monkeypatch.setattr(bot, "bot", fake_bot)
1690
+ seen = {}
1691
+
1692
+ async def resolve(*args, **kwargs):
1693
+ for _ in range(3):
1694
+ await asyncio.sleep(0)
1695
+ seen["typing_before_download"] = fake_bot.send_chat_action.await_count
1696
+ return "", None
1697
+
1698
+ monkeypatch.setattr(bot, "_resolve_incoming_media", resolve)
1699
+ _run_core_capturing_prompt(message, monkeypatch)
1700
+ assert seen.get("typing_before_download") == 1
1701
+
1702
+
1703
  def test_message_core_does_not_wait_for_typing_before_model(rate_guard_setup, monkeypatch):
1704
  # Что защищает: вызов модели не ждёт сетевого ответа на "печатает".
1705
  # Регрессия: вернуть await перед send_chat_action — модель стартует на один