maxapi-python 2.0.1__py3-none-any.whl → 2.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. {maxapi_python-2.0.1.dist-info → maxapi_python-2.1.1.dist-info}/METADATA +4 -1
  2. {maxapi_python-2.0.1.dist-info → maxapi_python-2.1.1.dist-info}/RECORD +62 -55
  3. pymax/__init__.py +1 -1
  4. pymax/api/auth/enums.py +13 -2
  5. pymax/api/auth/payloads.py +10 -6
  6. pymax/api/auth/service.py +75 -12
  7. pymax/api/bots/__init__.py +1 -0
  8. pymax/api/bots/payloads.py +7 -0
  9. pymax/api/bots/service.py +35 -0
  10. pymax/api/chats/enums.py +1 -0
  11. pymax/api/chats/payloads.py +14 -0
  12. pymax/api/chats/service.py +112 -12
  13. pymax/api/facade.py +2 -0
  14. pymax/api/messages/payloads.py +5 -1
  15. pymax/api/messages/service.py +36 -11
  16. pymax/api/self/service.py +29 -10
  17. pymax/api/session/payloads.py +9 -2
  18. pymax/api/uploads/models.py +1 -4
  19. pymax/api/uploads/payloads.py +9 -3
  20. pymax/api/uploads/service.py +107 -37
  21. pymax/api/users/service.py +15 -5
  22. pymax/app.py +19 -5
  23. pymax/auth/qr.py +11 -6
  24. pymax/auth/sms.py +13 -4
  25. pymax/base.py +1 -0
  26. pymax/client.py +4 -1
  27. pymax/client_web.py +4 -2
  28. pymax/config.py +11 -3
  29. pymax/connection/connection.py +15 -5
  30. pymax/connection/readers/tcp.py +4 -2
  31. pymax/dispatch/dispatcher.py +28 -10
  32. pymax/dispatch/mapping.py +9 -2
  33. pymax/dispatch/router.py +2 -0
  34. pymax/files/base.py +6 -1
  35. pymax/formatting/markdown.py +4 -1
  36. pymax/infra/auth.py +42 -0
  37. pymax/infra/base.py +2 -0
  38. pymax/infra/bots.py +33 -0
  39. pymax/infra/chat.py +102 -1
  40. pymax/infra/self.py +2 -6
  41. pymax/protocol/tcp/compression.py +3 -1
  42. pymax/protocol/tcp/framing.py +6 -11
  43. pymax/protocol/tcp/payload.py +3 -2
  44. pymax/protocol/tcp/protocol.py +13 -3
  45. pymax/protocol/ws/protocol.py +9 -3
  46. pymax/session/protocol.py +6 -2
  47. pymax/session/store.py +24 -8
  48. pymax/telemetry/navigation.py +3 -1
  49. pymax/telemetry/service.py +9 -3
  50. pymax/transport/tcp.py +14 -6
  51. pymax/transport/websocket.py +0 -2
  52. pymax/types/domain/__init__.py +3 -0
  53. pymax/types/domain/bots.py +14 -0
  54. pymax/types/domain/error.py +3 -3
  55. pymax/types/domain/folder.py +1 -1
  56. pymax/types/domain/login.py +18 -6
  57. pymax/types/domain/member.py +16 -0
  58. pymax/types/domain/presence.py +15 -0
  59. pymax/types/domain/sync.py +21 -5
  60. pymax/types/domain/user.py +12 -0
  61. {maxapi_python-2.0.1.dist-info → maxapi_python-2.1.1.dist-info}/WHEEL +0 -0
  62. {maxapi_python-2.0.1.dist-info → maxapi_python-2.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -38,16 +38,26 @@ logger = get_logger(__name__)
38
38
  class UploadService:
39
39
  def __init__(self, app: App) -> None:
40
40
  self.app = app
41
- self.video_upload_waiters: dict[int, asyncio.Future[VideoUploadSignal]] = {}
42
- self.file_upload_waiters: dict[int, asyncio.Future[FileUploadSignal]] = {}
43
- self.app.dispatcher.on_internal(EventType.VIDEO_READY)(self.on_video_attach)
44
- self.app.dispatcher.on_internal(EventType.FILE_READY)(self.on_file_attach)
41
+ self.video_upload_waiters: dict[
42
+ int, asyncio.Future[VideoUploadSignal]
43
+ ] = {}
44
+ self.file_upload_waiters: dict[
45
+ int, asyncio.Future[FileUploadSignal]
46
+ ] = {}
47
+ self.app.dispatcher.on_internal(EventType.VIDEO_READY)(
48
+ self.on_video_attach
49
+ )
50
+ self.app.dispatcher.on_internal(EventType.FILE_READY)(
51
+ self.on_file_attach
52
+ )
45
53
 
46
- async def upload_photo(self, photo: Photo) -> AttachPhotoPayload:
54
+ async def upload_photo(
55
+ self, photo: Photo, profile: bool = False
56
+ ) -> AttachPhotoPayload:
47
57
  logger.info("Uploading photo")
48
58
  logger.debug("Preparing photo upload payload")
49
59
 
50
- payload = UploadPayload().model_dump()
60
+ payload = UploadPayload(profile=profile).model_dump()
51
61
 
52
62
  try:
53
63
  data = await self.app.invoke(
@@ -62,7 +72,9 @@ class UploadService:
62
72
  url = payload_item(data, "url", str) # TODO: ENUM!!!!
63
73
  except Exception as e:
64
74
  logger.exception("Failed to parse photo upload URL from response")
65
- raise UploadError("Failed to parse photo upload URL from response") from e
75
+ raise UploadError(
76
+ "Failed to parse photo upload URL from response"
77
+ ) from e
66
78
 
67
79
  if not url:
68
80
  logger.error("No upload URL received")
@@ -80,11 +92,15 @@ class UploadService:
80
92
  except (KeyError, IndexError) as e:
81
93
  logger.exception("Photo upload URL does not contain photoIds")
82
94
  logger.debug("Invalid photo upload URL=%s", url)
83
- raise UploadError("Photo upload URL does not contain photoIds") from e
95
+ raise UploadError(
96
+ "Photo upload URL does not contain photoIds"
97
+ ) from e
84
98
  except Exception as e:
85
99
  logger.exception("Failed to parse photo id from upload URL")
86
100
  logger.debug("Invalid photo upload URL=%s", url)
87
- raise UploadError("Failed to parse photo id from upload URL") from e
101
+ raise UploadError(
102
+ "Failed to parse photo id from upload URL"
103
+ ) from e
88
104
 
89
105
  logger.debug("Photo upload id parsed photo_id=%s", photo_id)
90
106
 
@@ -122,23 +138,33 @@ class UploadService:
122
138
 
123
139
  try:
124
140
  async with (
125
- aiohttp.ClientSession() as session,
141
+ aiohttp.ClientSession(proxy=self.app.config.proxy) as session,
126
142
  session.post(
127
143
  url=url,
128
144
  data=form,
129
145
  ) as response,
130
146
  ):
131
- logger.debug("Photo upload HTTP response status=%s", response.status)
147
+ logger.debug(
148
+ "Photo upload HTTP response status=%s", response.status
149
+ )
132
150
 
133
151
  if response.status != HTTPStatus.OK:
134
- logger.error("Photo upload failed with status %s", response.status)
135
- raise UploadError(f"Photo upload failed with status {response.status}")
152
+ logger.error(
153
+ "Photo upload failed with status %s", response.status
154
+ )
155
+ raise UploadError(
156
+ f"Photo upload failed with status {response.status}"
157
+ )
136
158
 
137
159
  try:
138
160
  result = await response.json()
139
161
  except Exception as e:
140
- logger.exception("Failed to decode photo upload response JSON")
141
- raise UploadError("Failed to decode photo upload response JSON") from e
162
+ logger.exception(
163
+ "Failed to decode photo upload response JSON"
164
+ )
165
+ raise UploadError(
166
+ "Failed to decode photo upload response JSON"
167
+ ) from e
142
168
 
143
169
  except UploadError:
144
170
  raise
@@ -251,8 +277,12 @@ class UploadService:
251
277
  logger.debug("Video upload waiter registered video_id=%s", video_id)
252
278
 
253
279
  try:
254
- async with aiohttp.ClientSession(timeout=timeout) as session:
255
- logger.debug("Starting video upload HTTP request video_id=%s", video_id)
280
+ async with aiohttp.ClientSession(
281
+ timeout=timeout, proxy=self.app.config.proxy
282
+ ) as session:
283
+ logger.debug(
284
+ "Starting video upload HTTP request video_id=%s", video_id
285
+ )
256
286
 
257
287
  async with session.post(
258
288
  url=upload_info.url,
@@ -297,14 +327,26 @@ class UploadService:
297
327
  except UploadError:
298
328
  raise
299
329
  except aiohttp.ClientError as e:
300
- logger.exception("HTTP error during video upload video_id=%s", video_id)
301
- raise UploadError(f"HTTP error during video upload video_id={video_id}") from e
330
+ logger.exception(
331
+ "HTTP error during video upload video_id=%s", video_id
332
+ )
333
+ raise UploadError(
334
+ f"HTTP error during video upload video_id={video_id}"
335
+ ) from e
302
336
  except asyncio.TimeoutError as e:
303
- logger.exception("Timed out during video upload video_id=%s", video_id)
304
- raise UploadError(f"Timed out during video upload video_id={video_id}") from e
337
+ logger.exception(
338
+ "Timed out during video upload video_id=%s", video_id
339
+ )
340
+ raise UploadError(
341
+ f"Timed out during video upload video_id={video_id}"
342
+ ) from e
305
343
  except Exception as e:
306
- logger.exception("Unexpected error during video upload video_id=%s", video_id)
307
- raise UploadError(f"Unexpected error during video upload video_id={video_id}") from e
344
+ logger.exception(
345
+ "Unexpected error during video upload video_id=%s", video_id
346
+ )
347
+ raise UploadError(
348
+ f"Unexpected error during video upload video_id={video_id}"
349
+ ) from e
308
350
  finally:
309
351
  self.video_upload_waiters.pop(video_id, None)
310
352
  logger.debug("Video upload waiter removed video_id=%s", video_id)
@@ -371,7 +413,9 @@ class UploadService:
371
413
  logger.debug("File upload waiter registered file_id=%s", file_id)
372
414
 
373
415
  try:
374
- async with aiohttp.ClientSession() as session:
416
+ async with aiohttp.ClientSession(
417
+ proxy=self.app.config.proxy,
418
+ ) as session:
375
419
  async with session.post(
376
420
  url=upload_info.url,
377
421
  headers=headers,
@@ -414,44 +458,70 @@ class UploadService:
414
458
  except UploadError:
415
459
  raise
416
460
  except aiohttp.ClientError as e:
417
- logger.exception("HTTP error during file upload file_id=%s", file_id)
418
- raise UploadError(f"HTTP error during file upload file_id={file_id}") from e
461
+ logger.exception(
462
+ "HTTP error during file upload file_id=%s", file_id
463
+ )
464
+ raise UploadError(
465
+ f"HTTP error during file upload file_id={file_id}"
466
+ ) from e
419
467
  except asyncio.TimeoutError as e:
420
- logger.exception("Timed out during file upload file_id=%s", file_id)
421
- raise UploadError(f"Timed out during file upload file_id={file_id}") from e
468
+ logger.exception(
469
+ "Timed out during file upload file_id=%s", file_id
470
+ )
471
+ raise UploadError(
472
+ f"Timed out during file upload file_id={file_id}"
473
+ ) from e
422
474
  except Exception as e:
423
- logger.exception("Unexpected error during file upload file_id=%s", file_id)
424
- raise UploadError(f"Unexpected error during file upload file_id={file_id}") from e
475
+ logger.exception(
476
+ "Unexpected error during file upload file_id=%s", file_id
477
+ )
478
+ raise UploadError(
479
+ f"Unexpected error during file upload file_id={file_id}"
480
+ ) from e
425
481
  finally:
426
482
  self.file_upload_waiters.pop(file_id, None)
427
483
  logger.debug("File upload waiter removed file=%s", file_id)
428
484
 
429
- async def on_video_attach(self, attach: VideoUploadSignal, _: Client) -> None:
485
+ async def on_video_attach(
486
+ self, attach: VideoUploadSignal, _: Client
487
+ ) -> None:
430
488
  logger.debug("Received attach event video_id=%s", attach.video_id)
431
489
 
432
490
  future = self.video_upload_waiters.pop(attach.video_id, None)
433
491
 
434
492
  if not future:
435
- logger.debug("No video upload waiter found video_id=%s", attach.video_id)
493
+ logger.debug(
494
+ "No video upload waiter found video_id=%s", attach.video_id
495
+ )
436
496
  return
437
497
 
438
498
  if future.done():
439
- logger.debug("Video upload waiter already done video_id=%s", attach.video_id)
499
+ logger.debug(
500
+ "Video upload waiter already done video_id=%s", attach.video_id
501
+ )
440
502
  return
441
503
 
442
504
  future.set_result(attach)
443
- logger.debug("Video upload waiter resolved video_id=%s", attach.video_id)
505
+ logger.debug(
506
+ "Video upload waiter resolved video_id=%s", attach.video_id
507
+ )
444
508
 
445
- async def on_file_attach(self, attach: FileUploadSignal, _: Client) -> None:
509
+ async def on_file_attach(
510
+ self, attach: FileUploadSignal, _: Client
511
+ ) -> None:
446
512
  logger.debug("Received attach event file_id=%s", attach.file_id)
447
513
  future = self.file_upload_waiters.pop(attach.file_id, None)
448
514
 
449
515
  if not future:
450
- logger.debug("No file upload waiter found file_id=%s", attach.file_id)
516
+ logger.debug(
517
+ "No file upload waiter found file_id=%s", attach.file_id
518
+ )
451
519
  return
452
520
 
453
521
  if future.done():
454
- logger.debug("File upload waiter already done file_id=%s", attach.file_id)
522
+ logger.debug(
523
+ "File upload waiter already done file_id=%s", attach.file_id
524
+ )
455
525
  return
456
526
 
457
527
  future.set_result(attach)
@@ -44,7 +44,9 @@ class UserService:
44
44
  for user_id in user_ids
45
45
  if (user := self.get_cached_user(user_id)) is not None
46
46
  }
47
- missing_ids = [user_id for user_id in user_ids if user_id not in cached]
47
+ missing_ids = [
48
+ user_id for user_id in user_ids if user_id not in cached
49
+ ]
48
50
 
49
51
  if missing_ids:
50
52
  for user in await self.fetch_users(missing_ids):
@@ -62,11 +64,15 @@ class UserService:
62
64
  async def fetch_users(self, user_ids: list[int]) -> list[User]:
63
65
  logger.info("fetching users count=%s", len(user_ids))
64
66
  frame = FetchContactsPayload(contact_ids=user_ids)
65
- response = await self.app.invoke(Opcode.CONTACT_INFO, frame.to_payload())
67
+ response = await self.app.invoke(
68
+ Opcode.CONTACT_INFO, frame.to_payload()
69
+ )
66
70
 
67
71
  users = [
68
72
  self._cache_user(user)
69
- for user in parse_payload_list(response, UserPayloadKey.CONTACTS, User)
73
+ for user in parse_payload_list(
74
+ response, UserPayloadKey.CONTACTS, User
75
+ )
70
76
  ]
71
77
  logger.debug("fetched users count=%s", len(users))
72
78
  return users
@@ -91,8 +97,12 @@ class UserService:
91
97
  response = await self.app.invoke(Opcode.SESSIONS_INFO, {})
92
98
  return parse_payload_list(response, UserPayloadKey.SESSIONS, Session)
93
99
 
94
- async def _contact_action(self, payload: ContactActionPayload) -> InboundFrame:
95
- response = await self.app.invoke(Opcode.CONTACT_UPDATE, payload.to_payload())
100
+ async def _contact_action(
101
+ self, payload: ContactActionPayload
102
+ ) -> InboundFrame:
103
+ response = await self.app.invoke(
104
+ Opcode.CONTACT_UPDATE, payload.to_payload()
105
+ )
96
106
  require_payload_dict(response)
97
107
  return response
98
108
 
pymax/app.py CHANGED
@@ -33,7 +33,9 @@ class App(Generic[ClientT]):
33
33
  self.dispatcher: Dispatcher[ClientT] = Dispatcher(self, root_router)
34
34
  self.api = ApiFacade(self)
35
35
  self.config = config
36
- self.store = self.config.store or SessionStore(config.work_dir, config.session_name)
36
+ self.store = self.config.store or SessionStore(
37
+ config.work_dir, config.session_name
38
+ )
37
39
  self.auth_flow = auth_flow
38
40
 
39
41
  self.me: Profile | None = None
@@ -74,14 +76,18 @@ class App(Generic[ClientT]):
74
76
  await self.connection.open()
75
77
 
76
78
  handshake_device_id = (
77
- session_data.device_id if session_data else self.config.device.device_id
79
+ session_data.device_id
80
+ if session_data
81
+ else self.config.device.device_id
78
82
  )
79
83
  logger.debug("running handshake")
80
84
  await self.handshake(handshake_device_id)
81
85
  except (ConnectionError, EOFError, OSError, TimeoutError) as e:
82
86
  logger.exception("failed to connect or handshake")
83
87
  await self.connection.close()
84
- raise ConnectionError(f"Failed to connect and handshake: {e}") from e
88
+ raise ConnectionError(
89
+ f"Failed to connect and handshake: {e}"
90
+ ) from e
85
91
 
86
92
  self._ping_task = asyncio.create_task(self._ping_loop())
87
93
 
@@ -102,7 +108,9 @@ class App(Generic[ClientT]):
102
108
 
103
109
  if not auth_result.token:
104
110
  logger.error("authentication finished without token")
105
- raise RuntimeError("Authentication failed: no token received")
111
+ raise RuntimeError(
112
+ "Authentication failed: no token received"
113
+ )
106
114
 
107
115
  await self.store.save_session(
108
116
  session_data := SessionInfo(
@@ -127,6 +135,10 @@ class App(Generic[ClientT]):
127
135
  self.config.device.user_agent,
128
136
  )
129
137
 
138
+ if response.token != self.session.token:
139
+ await self.store.update_token(self.session.token, response.token)
140
+ self.session.token = response.token
141
+
130
142
  self.me = response.profile
131
143
  self.chats = response.chats
132
144
  self.users[self.me.contact.id] = self.me.contact
@@ -200,7 +212,9 @@ class App(Generic[ClientT]):
200
212
  )
201
213
  logger.debug("Request data=%s", frame.model_dump())
202
214
  response = await self.connection.request(frame, timeout=timeout)
203
- response_keys = sorted(response.payload.keys()) if response.payload else []
215
+ response_keys = (
216
+ sorted(response.payload.keys()) if response.payload else []
217
+ )
204
218
  logger.debug(
205
219
  "response opcode=%s cmd=%s seq=%s payload_keys=%s",
206
220
  response.opcode,
pymax/auth/qr.py CHANGED
@@ -4,8 +4,7 @@ import asyncio
4
4
  import time
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- import qrcode
8
-
7
+ from pymax.auth.base import AuthFlow
9
8
  from pymax.exceptions import ApiError
10
9
  from pymax.logging import get_logger
11
10
 
@@ -20,7 +19,7 @@ if TYPE_CHECKING:
20
19
  logger = get_logger(__name__)
21
20
 
22
21
 
23
- class QrAuthFlow:
22
+ class QrAuthFlow(AuthFlow):
24
23
  """Стандартная QR-авторизация ``WebClient``.
25
24
 
26
25
  Flow получает QR-ссылку, передает ее в ``QrHandler``, ждет подтверждения и
@@ -119,17 +118,23 @@ class QrAuthFlow:
119
118
  continue
120
119
 
121
120
  try:
122
- response = await app.api.auth.check_password(track_id, password)
121
+ response = await app.api.auth.check_password(
122
+ track_id, password
123
+ )
123
124
  except ApiError as e:
124
125
  logger.error("2fa password check failed: %s", e)
125
126
  continue
126
127
 
127
128
  if response.error:
128
- logger.error("2fa password check failed error=%s", response.error)
129
+ logger.error(
130
+ "2fa password check failed error=%s", response.error
131
+ )
129
132
  continue
130
133
 
131
134
  if response.login_token:
132
135
  logger.info("2fa password authentication completed")
133
136
  return response.login_token
134
137
 
135
- logger.error("2fa password response did not contain login token; retrying")
138
+ logger.error(
139
+ "2fa password response did not contain login token; retrying"
140
+ )
pymax/auth/sms.py CHANGED
@@ -2,11 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
+ from pymax.auth.base import AuthFlow
5
6
  from pymax.exceptions import ApiError
6
7
  from pymax.logging import get_logger
7
8
 
8
9
  from .models import AuthResult
9
- from .providers import ConsolePasswordProvider, PasswordProvider, SmsCodeProvider
10
+ from .providers import (
11
+ ConsolePasswordProvider,
12
+ PasswordProvider,
13
+ SmsCodeProvider,
14
+ )
10
15
 
11
16
  if TYPE_CHECKING:
12
17
  from pymax.app import App
@@ -15,7 +20,7 @@ if TYPE_CHECKING:
15
20
  logger = get_logger(__name__)
16
21
 
17
22
 
18
- class SmsAuthFlow:
23
+ class SmsAuthFlow(AuthFlow):
19
24
  """Стандартная SMS-авторизация ``Client``.
20
25
 
21
26
  Flow запрашивает SMS-код, отправляет его в Max, при необходимости проходит
@@ -104,13 +109,17 @@ class SmsAuthFlow:
104
109
  continue
105
110
 
106
111
  try:
107
- response = await app.api.auth.check_password(track_id, password)
112
+ response = await app.api.auth.check_password(
113
+ track_id, password
114
+ )
108
115
  except ApiError as e:
109
116
  logger.error("2fa password check failed: %s", e)
110
117
  continue
111
118
 
112
119
  if response.error:
113
- logger.error("2fa password check failed error=%s", response.error)
120
+ logger.error(
121
+ "2fa password check failed error=%s", response.error
122
+ )
114
123
  continue
115
124
 
116
125
  if response.login_token:
pymax/base.py CHANGED
@@ -82,6 +82,7 @@ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
82
82
  telemetry=self.extra_config.telemetry,
83
83
  sync=self.extra_config.sync,
84
84
  store=self.extra_config.store,
85
+ proxy=self.extra_config.proxy,
85
86
  device=DeviceConfig(
86
87
  mt_instance_id=self.extra_config.mt_instance_id,
87
88
  device_id=self.extra_config.device_id or str(uuid4()),
pymax/client.py CHANGED
@@ -66,7 +66,10 @@ class Client(BaseClient["Client"]):
66
66
 
67
67
  self._config = self._build_config(
68
68
  phone=phone,
69
- user_agent=(self.extra_config.user_agent or self.extra_config.generate_user_agent()),
69
+ user_agent=(
70
+ self.extra_config.user_agent
71
+ or self.extra_config.generate_user_agent()
72
+ ),
70
73
  )
71
74
 
72
75
  if auth_flow is None:
pymax/client_web.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from pymax.auth.base import AuthFlow
3
4
  from pymax.auth.providers import ConsoleQrHandler, QrHandler
4
5
  from pymax.auth.qr import QrAuthFlow
5
6
  from pymax.connection import ConnectionManager
@@ -34,7 +35,7 @@ class WebClient(BaseClient["WebClient"]):
34
35
  session_name: str = "session.db",
35
36
  work_dir: str = ".",
36
37
  extra_config: ExtraConfig | None = None,
37
- auth_flow: QrAuthFlow | None = None,
38
+ auth_flow: AuthFlow | None = None,
38
39
  qr_provider: QrHandler | None = None,
39
40
  ) -> None:
40
41
  self.extra_config = extra_config or ExtraConfig()
@@ -53,7 +54,8 @@ class WebClient(BaseClient["WebClient"]):
53
54
  self._config = self._build_config(
54
55
  phone=None,
55
56
  user_agent=(
56
- self.extra_config.user_agent or self.extra_config.generate_web_user_agent()
57
+ self.extra_config.user_agent
58
+ or self.extra_config.generate_web_user_agent()
57
59
  ),
58
60
  )
59
61
 
pymax/config.py CHANGED
@@ -8,7 +8,7 @@ from pymax.api.session.payloads import (
8
8
  DEFAULT_WEB_HEADER_USER_AGENT,
9
9
  MobileUserAgentPayload,
10
10
  )
11
- from pymax.session import SessionStore, StoreProtocol
11
+ from pymax.session import StoreProtocol
12
12
  from pymax.types.domain.sync import SyncOverrides
13
13
 
14
14
  APP_VERSIONS: tuple[tuple[str, int], ...] = (
@@ -35,7 +35,12 @@ ANDROID_DEVICES: tuple[tuple[str, str, str, str], ...] = (
35
35
  ("Xiaomi 2201117TG", "Android 13", "395dpi 395dpi 1080x2400", "arm64-v8a"),
36
36
  ("Xiaomi 2201123G", "Android 14", "526dpi 526dpi 1440x3200", "arm64-v8a"),
37
37
  ("Xiaomi 2210132G", "Android 14", "446dpi 446dpi 1220x2712", "arm64-v8a"),
38
- ("Xiaomi 23049PCD8G", "Android 14", "446dpi 446dpi 1220x2712", "arm64-v8a"),
38
+ (
39
+ "Xiaomi 23049PCD8G",
40
+ "Android 14",
41
+ "446dpi 446dpi 1220x2712",
42
+ "arm64-v8a",
43
+ ),
39
44
  ("Redmi 2201116TG", "Android 13", "395dpi 395dpi 1080x2400", "arm64-v8a"),
40
45
  ("Redmi 22101316G", "Android 13", "395dpi 395dpi 1080x2400", "arm64-v8a"),
41
46
  ("Redmi 23021RAA2Y", "Android 14", "395dpi 395dpi 1080x2400", "arm64-v8a"),
@@ -87,6 +92,7 @@ class ClientConfig(BaseModel):
87
92
  session_name: str = "session.db"
88
93
  device: DeviceConfig
89
94
  token: str | None = None
95
+ proxy: str | None = None
90
96
 
91
97
  host: str = "api.oneme.ru"
92
98
  port: int = 443
@@ -103,7 +109,9 @@ class ClientConfig(BaseModel):
103
109
 
104
110
  def ensure_config(self) -> None:
105
111
  if not self.phone:
106
- raise ValueError("Phone must be provided when no saved session exists.")
112
+ raise ValueError(
113
+ "Phone must be provided when no saved session exists."
114
+ )
107
115
 
108
116
 
109
117
  class ExtraConfig(BaseModel):
@@ -80,6 +80,7 @@ class ConnectionManager:
80
80
  self._connection_lost = True
81
81
  self.requests.cancel_all(exc=exc)
82
82
  await self.transport.close()
83
+ self._is_open = False
83
84
 
84
85
  async def send(self, frame: OutboundFrame) -> None:
85
86
  if not self._is_open:
@@ -145,19 +146,28 @@ class ConnectionManager:
145
146
  model = self.protocol.decode(frame)
146
147
  await self._handle_inbound(model)
147
148
 
148
- except EOFError as e:
149
+ except EOFError:
149
150
  logger.warning("connection closed by server")
150
- self.requests.cancel_all(exc=ConnectionError("Connection closed by the server"))
151
+ self.requests.cancel_all(
152
+ exc=ConnectionError("Connection closed by the server")
153
+ )
151
154
  self._connection_lost = True
155
+ self._is_open = False
152
156
  except TimeoutError as e:
153
157
  logger.exception("connection timed out")
154
- self.requests.cancel_all(exc=ConnectionError("Connection timed out"))
158
+ self.requests.cancel_all(
159
+ exc=ConnectionError("Connection timed out")
160
+ )
155
161
  self._connection_lost = True
162
+ self._is_open = False
156
163
  raise e
157
164
  except Exception as e:
158
165
  logger.exception("connection receive loop failed")
159
- self.requests.cancel_all(exc=ConnectionError(f"Connection error: {e}"))
166
+ self.requests.cancel_all(
167
+ exc=ConnectionError(f"Connection error: {e}")
168
+ )
160
169
  self._connection_lost = True
170
+ self._is_open = False
161
171
  raise e
162
172
 
163
173
  async def _handle_inbound(self, frame: InboundFrame) -> None:
@@ -197,7 +207,7 @@ class ConnectionManager:
197
207
  )
198
208
 
199
209
  def next_seq(self) -> int:
200
- self._seq = (self._seq + 1) % 0xFFFFFFFF
210
+ self._seq = (self._seq + 1) % 0x10000
201
211
  return self._seq
202
212
 
203
213
  @property
@@ -8,13 +8,15 @@ logger = get_logger(__name__)
8
8
 
9
9
 
10
10
  class TCPReader(BaseReader):
11
- def __init__(self, transport: TCPTransport, framer: TcpPacketFramer) -> None:
11
+ def __init__(
12
+ self, transport: TCPTransport, framer: TcpPacketFramer
13
+ ) -> None:
12
14
  super().__init__()
13
15
  self.transport = transport
14
16
  self.framer = framer
15
17
 
16
18
  async def read(self) -> bytes:
17
- header_bytes = await self.transport.recv(10)
19
+ header_bytes = await self.transport.recv(self.framer.HEADER_SIZE)
18
20
  payload_len = self.framer.unpack_header(header_bytes)
19
21
  if payload_len is None:
20
22
  logger.warning(