2525from utils .utils import (
2626 get_timestamp ,
2727 keep_typing ,
28+ serialize_user_update_by_id ,
2829 serialize_user_updates ,
2930 typing_action ,
3031)
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
5863def _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+
133182def _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