Skip to content

Commit dc2a2bc

Browse files
committed
feat(connect): proactive phone code timeout notification
Previously the bot only checked code expiration reactively — when the user sent the next message. Now a background asyncio task notifies the user as soon as PHONE_CODE_TIMEOUT_SECONDS elapses, and cleans up the phone-flow automatically. Changes: - Add _phone_timeout_tasks dict for background timeout tasks - Add _start_phone_timeout_task() / _cancel_phone_timeout_task() - Start timer after code is sent (on_confirm_phone_callback) - Restart timer on PhoneCodeInvalid (expires_at is extended) - Cancel timer on 2FA transition and in _finalize_phone_login - Self-pop from dict before cancel_pending_phone to avoid self-cancellation; try/finally ensures cleanup on send failure Files changed: - handlers/connect_handler.py
1 parent 80e0750 commit dc2a2bc

File tree

1 file changed

+56
-0
lines changed

1 file changed

+56
-0
lines changed

handlers/connect_handler.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from utils.utils import (
2626
get_timestamp,
2727
keep_typing,
28+
serialize_user_update_by_id,
2829
serialize_user_updates,
2930
typing_action,
3031
)
@@ -54,6 +55,10 @@
5455
# {user_id: {"state": str, "client": Client, ..., "sensitive_msg_ids": list[int], "expires_at": float}}
5556
_pending_phone: dict[int, dict] = {}
5657

58+
# Фоновые задачи таймаута для phone code (проактивное уведомление)
59+
# {user_id: asyncio.Task}
60+
_phone_timeout_tasks: dict[int, asyncio.Task] = {}
61+
5762

5863
def _get_chat_type(update: Update) -> str | None:
5964
"""Возвращает тип чата Telegram как строку."""
@@ -113,6 +118,7 @@ async def cancel_pending_phone(
113118
user_id: int, bot: Bot | None = None, client: Client | None = None,
114119
) -> bool:
115120
"""Отменяет незавершённый phone-логин, удаляет sensitive messages и чистит клиент."""
121+
_cancel_phone_timeout_task(user_id)
116122
pending = _pending_phone.pop(user_id, None)
117123

118124
# Удаляем чувствительные сообщения при отмене/таймауте
@@ -130,6 +136,49 @@ async def cancel_pending_phone(
130136
return pending is not None
131137

132138

139+
def _cancel_phone_timeout_task(user_id: int) -> None:
140+
"""Отменяет фоновую задачу таймаута phone code, если она есть."""
141+
task = _phone_timeout_tasks.pop(user_id, None)
142+
if task is not None and not task.done():
143+
task.cancel()
144+
145+
146+
def _start_phone_timeout_task(
147+
user_id: int, language_code: str, bot: Bot, chat_id: int,
148+
) -> None:
149+
"""Запускает фоновый таймер: по истечении PHONE_CODE_TIMEOUT_SECONDS отправит сообщение."""
150+
_cancel_phone_timeout_task(user_id) # отменяем старый, если есть
151+
152+
async def _timeout_worker() -> None:
153+
try:
154+
await asyncio.sleep(PHONE_CODE_TIMEOUT_SECONDS)
155+
156+
async with serialize_user_update_by_id(user_id):
157+
pending = _pending_phone.get(user_id)
158+
if pending is None or pending.get("state") != "awaiting_code":
159+
return
160+
161+
print(f"{get_timestamp()} [CONNECT_PHONE] Code timeout for user {user_id}")
162+
# Убираем себя из словаря ДО cancel_pending_phone,
163+
# иначе _cancel_phone_timeout_task() отменит текущую задачу
164+
_phone_timeout_tasks.pop(user_id, None)
165+
try:
166+
msg = await get_system_message(language_code, "connect_code_expired")
167+
await bot.send_message(chat_id=chat_id, text=msg)
168+
finally:
169+
# Cleanup всегда, даже если send_message упал
170+
await cancel_pending_phone(user_id, bot=bot)
171+
except asyncio.CancelledError:
172+
return
173+
finally:
174+
# Если воркер завершился без явного pop (early return), чистим за собой
175+
task = _phone_timeout_tasks.get(user_id)
176+
if task is asyncio.current_task():
177+
_phone_timeout_tasks.pop(user_id, None)
178+
179+
_phone_timeout_tasks[user_id] = asyncio.create_task(_timeout_worker())
180+
181+
133182
def _get_qr_login_task(user_id: int) -> asyncio.Task | None:
134183
"""Возвращает активную QR-задачу пользователя."""
135184
entry = _qr_login_tasks.get(user_id)
@@ -528,6 +577,9 @@ async def on_confirm_phone_callback(update: Update, context: ContextTypes.DEFAUL
528577
# Запоминаем ID сообщения с просьбой ввести код
529578
sensitive_msg_ids.append(sent.message_id)
530579

580+
# Фоновый таймер — проактивно уведомим об истечении кода
581+
_start_phone_timeout_task(u.id, language_code, context.bot, chat_id)
582+
531583
if DEBUG_PRINT:
532584
print(f"{get_timestamp()} [CONNECT_PHONE] Code sent to user {u.id}")
533585

@@ -667,6 +719,7 @@ async def _handle_phone_code(
667719

668720
if "SessionPasswordNeeded" in error_name:
669721
# 2FA требуется — переводим в awaiting_2fa
722+
_cancel_phone_timeout_task(u.id)
670723
_put_pending_phone(u.id, {**_pending_phone[u.id], "state": "awaiting_2fa"})
671724
msg = await get_system_message(language_code, "connect_2fa_prompt")
672725
sent = await context.bot.send_message(chat_id=chat_id, text=msg)
@@ -679,6 +732,8 @@ async def _handle_phone_code(
679732
# Неверный код — остаёмся в awaiting_code, даём попробовать ещё раз
680733
print(f"{get_timestamp()} [CONNECT_PHONE] WARNING: invalid code for user {u.id}")
681734
_put_pending_phone(u.id, pending)
735+
# Перезапускаем таймер — _put_pending_phone обновил expires_at
736+
_start_phone_timeout_task(u.id, language_code, context.bot, chat_id)
682737
msg = await get_system_message(language_code, "connect_code_invalid")
683738
sent = await context.bot.send_message(chat_id=chat_id, text=msg)
684739
pending.setdefault("sensitive_msg_ids", []).append(sent.message_id)
@@ -795,6 +850,7 @@ async def _finalize_phone_login(
795850
print(f"{get_timestamp()} [BOT] User {user_id} connected via phone")
796851

797852
finally:
853+
_cancel_phone_timeout_task(user_id)
798854
_pending_phone.pop(user_id, None)
799855
await _safe_disconnect_temp_client(client, user_id)
800856
# Удаляем чувствительные сообщения после завершения (успех или ошибка)

0 commit comments

Comments
 (0)