33import socket
44import ssl
55import time
6+ import traceback
7+ from collections .abc import Awaitable
68from pathlib import Path
7- from typing import Any , Literal
9+ from typing import TYPE_CHECKING , Any , Literal
810
911from typing_extensions import override
1012
1113from .crud import Database
1214from .exceptions import InvalidPhoneError
15+ from .formatter import ColoredFormatter
1316from .mixins import ApiMixin , SocketMixin , WebSocketMixin
1417from .payloads import UserAgentPayload
1518from .static .constant import (
1821 WEBSOCKET_URI ,
1922)
2023
24+ if TYPE_CHECKING :
25+ from collections .abc import Awaitable , Callable
26+ from typing import Any
27+
28+ import websockets
29+
30+ from .filters import Filter
31+ from .types import Channel , Chat , Dialog , Me , Message , User
32+
33+
2134logger = logging .getLogger (__name__ )
2235
2336
@@ -64,9 +77,7 @@ def __init__(
6477 last_name : str | None = None ,
6578 logger : logging .Logger | None = None ,
6679 ) -> None :
67- logger = logger or logging .getLogger (f"{ __name__ } .MaxClient" )
68- ApiMixin .__init__ (self , token = token , logger = logger )
69- WebSocketMixin .__init__ (self , token = token , logger = logger )
80+ self .logger = logger or logging .getLogger (f"{ __name__ } " )
7081 self .uri : str = uri
7182 self .phone : str = phone
7283 if not self ._check_phone ():
@@ -77,32 +88,64 @@ def __init__(
7788 self .first_name : str = first_name
7889 self .last_name : str | None = last_name
7990 self .proxy : str | Literal [True ] | None = proxy
91+
92+ self .is_connected : bool = False
93+
94+ self .chats : list [Chat ] = []
95+ self .dialogs : list [Dialog ] = []
96+ self .channels : list [Channel ] = []
97+ self .me : Me | None = None
98+ self ._users : dict [int , User ] = {}
99+
80100 self ._work_dir : str = work_dir
81101 self ._database_path : Path = Path (work_dir ) / "session.db"
82102 self ._database_path .parent .mkdir (parents = True , exist_ok = True )
83103 self ._database_path .touch (exist_ok = True )
84104 self ._database = Database (self ._work_dir )
105+
106+ self ._incoming : asyncio .Queue [dict [str , Any ]] | None = None
85107 self ._outgoing : asyncio .Queue [dict [str , Any ]] | None = None
108+ self ._recv_task : asyncio .Task [Any ] | None = None
86109 self ._outgoing_task : asyncio .Task [Any ] | None = None
110+ self ._pending : dict [int , asyncio .Future [dict [str , Any ]]] = {}
111+ self ._file_upload_waiters : dict [int , asyncio .Future [dict [str , Any ]]] = {}
112+ self ._background_tasks : set [asyncio .Task [Any ]] = set ()
113+
114+ self ._seq : int = 0
87115 self ._error_count : int = 0
88116 self ._circuit_breaker : bool = False
89117 self ._last_error_time : float = 0.0
118+
90119 self ._device_id = self ._database .get_device_id ()
91- self ._file_upload_waiters : dict [
92- int , asyncio .Future [dict [str , Any ]]
93- ] = {}
120+ self ._file_upload_waiters : dict [int , asyncio .Future [dict [str , Any ]]] = {}
121+
94122 self ._token = self ._database .get_auth_token () or token
95123 self .user_agent = headers
96124 self ._send_fake_telemetry : bool = send_fake_telemetry
97125 self ._session_id : int = int (time .time () * 1000 )
98126 self ._action_id : int = 1
127+ self ._current_screen : str = "chats_list_tab"
128+
129+ self ._on_message_handlers : list [
130+ tuple [Callable [[Message ], Any ], Filter | None ]
131+ ] = []
132+ self ._on_message_edit_handlers : list [
133+ tuple [Callable [[Message ], Any ], Filter | None ]
134+ ] = []
135+ self ._on_message_delete_handlers : list [
136+ tuple [Callable [[Message ], Any ], Filter | None ]
137+ ] = []
138+ self ._on_start_handler : Callable [[], Any | Awaitable [Any ]] | None = None
139+
99140 self ._ssl_context = ssl .create_default_context ()
100141 self ._ssl_context .set_ciphers ("DEFAULT" )
101142 self ._ssl_context .check_hostname = True
102143 self ._ssl_context .verify_mode = ssl .CERT_REQUIRED
103144 self ._ssl_context .minimum_version = ssl .TLSVersion .TLSv1_2
104145 self ._ssl_context .load_default_certs ()
105146 self ._socket : socket .socket | None = None
147+ self ._ws : websockets .ClientConnection | None = None
148+
106149 self ._setup_logger ()
107150 self .logger .debug (
108151 "Initialized MaxClient uri=%s work_dir=%s" ,
@@ -111,20 +154,35 @@ def __init__(
111154 )
112155
113156 def _setup_logger (self ) -> None :
114- if not logger .handlers :
157+ if not self .logger .handlers :
158+ if not self .logger .level :
159+ self .logger .setLevel (logging .INFO )
115160 handler = logging .StreamHandler ()
116- formatter = logging .Formatter (
117- "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
161+ formatter = ColoredFormatter (
162+ "%(asctime)s [%(levelname)s] %(name)s: %(message)s" ,
163+ datefmt = "%Y-%m-%d %H:%M:%S" ,
118164 )
119165 handler .setFormatter (formatter )
120- logger .addHandler (handler )
166+ self . logger .addHandler (handler )
121167
122168 async def _wait_forever (self ):
123169 try :
124170 await self .ws .wait_closed ()
125171 except asyncio .CancelledError :
126172 self .logger .debug ("wait_closed cancelled" )
127173
174+ async def _safe_execute (self , coro , * , context : str = "unknown" ):
175+ """
176+ Безопасно выполняет пользовательскую корутину.
177+ Логирует traceback, но не роняет event loop.
178+ """
179+ try :
180+ return await coro
181+ except Exception as e :
182+ self .logger .error (
183+ f"Unhandled exception in { context } : { e } \n { traceback .format_exc ()} "
184+ )
185+
128186 async def close (self ) -> None :
129187 try :
130188 self .logger .info ("Closing client" )
@@ -147,6 +205,23 @@ async def close(self) -> None:
147205 except Exception :
148206 self .logger .exception ("Error closing client" )
149207
208+ def _create_safe_task (self , coro : Awaitable [Any ], * , name : str | None = None ):
209+ async def runner ():
210+ try :
211+ return await coro
212+ except asyncio .CancelledError :
213+ raise
214+ except Exception as e :
215+ self .logger .error (
216+ f"Unhandled exception in task { name or coro } : { e } " ,
217+ exc_info = e ,
218+ )
219+ return None
220+
221+ task = asyncio .create_task (runner (), name = name )
222+ self ._background_tasks .add (task )
223+ return task
224+
150225 async def start (self ) -> None :
151226 """
152227 Запускает клиент, подключается к WebSocket, авторизует
@@ -173,7 +248,7 @@ async def start(self) -> None:
173248 self .logger .debug ("Calling on_start handler" )
174249 result = self ._on_start_handler ()
175250 if asyncio .iscoroutine (result ):
176- await result
251+ await self . _safe_execute ( result , context = "on_start handler" )
177252
178253 ping_task = asyncio .create_task (self ._send_interactive_ping ())
179254 ping_task .add_done_callback (self ._log_task_exception )
0 commit comments