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.
- {maxapi_python-2.0.1.dist-info → maxapi_python-2.1.1.dist-info}/METADATA +4 -1
- {maxapi_python-2.0.1.dist-info → maxapi_python-2.1.1.dist-info}/RECORD +62 -55
- pymax/__init__.py +1 -1
- pymax/api/auth/enums.py +13 -2
- pymax/api/auth/payloads.py +10 -6
- pymax/api/auth/service.py +75 -12
- pymax/api/bots/__init__.py +1 -0
- pymax/api/bots/payloads.py +7 -0
- pymax/api/bots/service.py +35 -0
- pymax/api/chats/enums.py +1 -0
- pymax/api/chats/payloads.py +14 -0
- pymax/api/chats/service.py +112 -12
- pymax/api/facade.py +2 -0
- pymax/api/messages/payloads.py +5 -1
- pymax/api/messages/service.py +36 -11
- pymax/api/self/service.py +29 -10
- pymax/api/session/payloads.py +9 -2
- pymax/api/uploads/models.py +1 -4
- pymax/api/uploads/payloads.py +9 -3
- pymax/api/uploads/service.py +107 -37
- pymax/api/users/service.py +15 -5
- pymax/app.py +19 -5
- pymax/auth/qr.py +11 -6
- pymax/auth/sms.py +13 -4
- pymax/base.py +1 -0
- pymax/client.py +4 -1
- pymax/client_web.py +4 -2
- pymax/config.py +11 -3
- pymax/connection/connection.py +15 -5
- pymax/connection/readers/tcp.py +4 -2
- pymax/dispatch/dispatcher.py +28 -10
- pymax/dispatch/mapping.py +9 -2
- pymax/dispatch/router.py +2 -0
- pymax/files/base.py +6 -1
- pymax/formatting/markdown.py +4 -1
- pymax/infra/auth.py +42 -0
- pymax/infra/base.py +2 -0
- pymax/infra/bots.py +33 -0
- pymax/infra/chat.py +102 -1
- pymax/infra/self.py +2 -6
- pymax/protocol/tcp/compression.py +3 -1
- pymax/protocol/tcp/framing.py +6 -11
- pymax/protocol/tcp/payload.py +3 -2
- pymax/protocol/tcp/protocol.py +13 -3
- pymax/protocol/ws/protocol.py +9 -3
- pymax/session/protocol.py +6 -2
- pymax/session/store.py +24 -8
- pymax/telemetry/navigation.py +3 -1
- pymax/telemetry/service.py +9 -3
- pymax/transport/tcp.py +14 -6
- pymax/transport/websocket.py +0 -2
- pymax/types/domain/__init__.py +3 -0
- pymax/types/domain/bots.py +14 -0
- pymax/types/domain/error.py +3 -3
- pymax/types/domain/folder.py +1 -1
- pymax/types/domain/login.py +18 -6
- pymax/types/domain/member.py +16 -0
- pymax/types/domain/presence.py +15 -0
- pymax/types/domain/sync.py +21 -5
- pymax/types/domain/user.py +12 -0
- {maxapi_python-2.0.1.dist-info → maxapi_python-2.1.1.dist-info}/WHEEL +0 -0
- {maxapi_python-2.0.1.dist-info → maxapi_python-2.1.1.dist-info}/licenses/LICENSE +0 -0
pymax/api/uploads/service.py
CHANGED
|
@@ -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[
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
self.
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
135
|
-
|
|
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(
|
|
141
|
-
|
|
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(
|
|
255
|
-
|
|
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(
|
|
301
|
-
|
|
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(
|
|
304
|
-
|
|
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(
|
|
307
|
-
|
|
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(
|
|
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(
|
|
418
|
-
|
|
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(
|
|
421
|
-
|
|
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(
|
|
424
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
505
|
+
logger.debug(
|
|
506
|
+
"Video upload waiter resolved video_id=%s", attach.video_id
|
|
507
|
+
)
|
|
444
508
|
|
|
445
|
-
async def on_file_attach(
|
|
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(
|
|
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(
|
|
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)
|
pymax/api/users/service.py
CHANGED
|
@@ -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 = [
|
|
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(
|
|
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(
|
|
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(
|
|
95
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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=(
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
(
|
|
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(
|
|
112
|
+
raise ValueError(
|
|
113
|
+
"Phone must be provided when no saved session exists."
|
|
114
|
+
)
|
|
107
115
|
|
|
108
116
|
|
|
109
117
|
class ExtraConfig(BaseModel):
|
pymax/connection/connection.py
CHANGED
|
@@ -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
|
|
149
|
+
except EOFError:
|
|
149
150
|
logger.warning("connection closed by server")
|
|
150
|
-
self.requests.cancel_all(
|
|
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(
|
|
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(
|
|
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) %
|
|
210
|
+
self._seq = (self._seq + 1) % 0x10000
|
|
201
211
|
return self._seq
|
|
202
212
|
|
|
203
213
|
@property
|
pymax/connection/readers/tcp.py
CHANGED
|
@@ -8,13 +8,15 @@ logger = get_logger(__name__)
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class TCPReader(BaseReader):
|
|
11
|
-
def __init__(
|
|
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(
|
|
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(
|