maxapi-python 2.1.3__py3-none-any.whl → 2.2.0__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.1.3.dist-info → maxapi_python-2.2.0.dist-info}/METADATA +3 -11
- {maxapi_python-2.1.3.dist-info → maxapi_python-2.2.0.dist-info}/RECORD +58 -53
- pymax/__init__.py +18 -3
- pymax/api/auth/payloads.py +7 -0
- pymax/api/auth/service.py +33 -30
- pymax/api/binding.py +57 -0
- pymax/api/chats/service.py +34 -47
- pymax/api/messages/enums.py +1 -0
- pymax/api/messages/payloads.py +16 -1
- pymax/api/messages/service.py +85 -33
- pymax/api/models.py +4 -6
- pymax/api/response.py +2 -2
- pymax/api/self/service.py +17 -26
- pymax/api/session/payloads.py +2 -9
- pymax/api/session/service.py +1 -3
- pymax/api/uploads/payloads.py +3 -9
- pymax/api/uploads/service.py +33 -99
- pymax/api/users/service.py +8 -16
- pymax/app.py +2 -0
- pymax/auth/qr.py +3 -9
- pymax/auth/sms.py +23 -11
- pymax/base.py +38 -1
- pymax/client.py +2 -1
- pymax/client_web.py +1 -2
- pymax/config.py +42 -3
- pymax/connection/connection.py +2 -0
- pymax/connection/readers/tcp.py +1 -3
- pymax/dispatch/dispatcher.py +36 -18
- pymax/dispatch/enums.py +4 -0
- pymax/dispatch/mapping.py +34 -11
- pymax/dispatch/resolvers.py +18 -0
- pymax/dispatch/router.py +34 -0
- pymax/formatting/markdown.py +22 -13
- pymax/infra/chat.py +12 -0
- pymax/infra/message.py +74 -3
- pymax/logging.py +2 -0
- pymax/protocol/tcp/compression.py +1 -3
- pymax/protocol/tcp/framing.py +1 -3
- pymax/protocol/ws/protocol.py +3 -9
- pymax/session/protocol.py +2 -6
- pymax/session/store.py +8 -24
- pymax/telemetry/navigation.py +1 -3
- pymax/telemetry/service.py +5 -17
- pymax/transport/tcp.py +1 -3
- pymax/types/domain/attachments/unknown.py +1 -3
- pymax/types/domain/auth.py +24 -2
- pymax/types/domain/chat.py +38 -1
- pymax/types/domain/message.py +31 -1
- pymax/types/domain/presence.py +3 -3
- pymax/types/domain/sync.py +5 -21
- pymax/types/events/__init__.py +4 -0
- pymax/types/events/mark.py +23 -0
- pymax/types/events/message.py +57 -5
- pymax/types/events/presence.py +15 -0
- pymax/types/events/reaction.py +21 -0
- pymax/types/events/typing.py +14 -0
- {maxapi_python-2.1.3.dist-info → maxapi_python-2.2.0.dist-info}/WHEEL +0 -0
- {maxapi_python-2.1.3.dist-info → maxapi_python-2.2.0.dist-info}/licenses/LICENSE +0 -0
pymax/api/uploads/service.py
CHANGED
|
@@ -38,22 +38,12 @@ 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.
|
|
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
|
-
)
|
|
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)
|
|
53
45
|
|
|
54
|
-
async def upload_photo(
|
|
55
|
-
self, photo: Photo, profile: bool = False
|
|
56
|
-
) -> AttachPhotoPayload:
|
|
46
|
+
async def upload_photo(self, photo: Photo, profile: bool = False) -> AttachPhotoPayload:
|
|
57
47
|
logger.info("Uploading photo")
|
|
58
48
|
logger.debug("Preparing photo upload payload")
|
|
59
49
|
|
|
@@ -72,9 +62,7 @@ class UploadService:
|
|
|
72
62
|
url = payload_item(data, "url", str) # TODO: ENUM!!!!
|
|
73
63
|
except Exception as e:
|
|
74
64
|
logger.exception("Failed to parse photo upload URL from response")
|
|
75
|
-
raise UploadError(
|
|
76
|
-
"Failed to parse photo upload URL from response"
|
|
77
|
-
) from e
|
|
65
|
+
raise UploadError("Failed to parse photo upload URL from response") from e
|
|
78
66
|
|
|
79
67
|
if not url:
|
|
80
68
|
logger.error("No upload URL received")
|
|
@@ -92,15 +80,11 @@ class UploadService:
|
|
|
92
80
|
except (KeyError, IndexError) as e:
|
|
93
81
|
logger.exception("Photo upload URL does not contain photoIds")
|
|
94
82
|
logger.debug("Invalid photo upload URL=%s", url)
|
|
95
|
-
raise UploadError(
|
|
96
|
-
"Photo upload URL does not contain photoIds"
|
|
97
|
-
) from e
|
|
83
|
+
raise UploadError("Photo upload URL does not contain photoIds") from e
|
|
98
84
|
except Exception as e:
|
|
99
85
|
logger.exception("Failed to parse photo id from upload URL")
|
|
100
86
|
logger.debug("Invalid photo upload URL=%s", url)
|
|
101
|
-
raise UploadError(
|
|
102
|
-
"Failed to parse photo id from upload URL"
|
|
103
|
-
) from e
|
|
87
|
+
raise UploadError("Failed to parse photo id from upload URL") from e
|
|
104
88
|
|
|
105
89
|
logger.debug("Photo upload id parsed photo_id=%s", photo_id)
|
|
106
90
|
|
|
@@ -144,27 +128,17 @@ class UploadService:
|
|
|
144
128
|
data=form,
|
|
145
129
|
) as response,
|
|
146
130
|
):
|
|
147
|
-
logger.debug(
|
|
148
|
-
"Photo upload HTTP response status=%s", response.status
|
|
149
|
-
)
|
|
131
|
+
logger.debug("Photo upload HTTP response status=%s", response.status)
|
|
150
132
|
|
|
151
133
|
if response.status != HTTPStatus.OK:
|
|
152
|
-
logger.error(
|
|
153
|
-
|
|
154
|
-
)
|
|
155
|
-
raise UploadError(
|
|
156
|
-
f"Photo upload failed with status {response.status}"
|
|
157
|
-
)
|
|
134
|
+
logger.error("Photo upload failed with status %s", response.status)
|
|
135
|
+
raise UploadError(f"Photo upload failed with status {response.status}")
|
|
158
136
|
|
|
159
137
|
try:
|
|
160
138
|
result = await response.json()
|
|
161
139
|
except Exception as e:
|
|
162
|
-
logger.exception(
|
|
163
|
-
|
|
164
|
-
)
|
|
165
|
-
raise UploadError(
|
|
166
|
-
"Failed to decode photo upload response JSON"
|
|
167
|
-
) from e
|
|
140
|
+
logger.exception("Failed to decode photo upload response JSON")
|
|
141
|
+
raise UploadError("Failed to decode photo upload response JSON") from e
|
|
168
142
|
|
|
169
143
|
except UploadError:
|
|
170
144
|
raise
|
|
@@ -280,9 +254,7 @@ class UploadService:
|
|
|
280
254
|
async with aiohttp.ClientSession(
|
|
281
255
|
timeout=timeout, proxy=self.app.config.proxy
|
|
282
256
|
) as session:
|
|
283
|
-
logger.debug(
|
|
284
|
-
"Starting video upload HTTP request video_id=%s", video_id
|
|
285
|
-
)
|
|
257
|
+
logger.debug("Starting video upload HTTP request video_id=%s", video_id)
|
|
286
258
|
|
|
287
259
|
async with session.post(
|
|
288
260
|
url=upload_info.url,
|
|
@@ -327,26 +299,14 @@ class UploadService:
|
|
|
327
299
|
except UploadError:
|
|
328
300
|
raise
|
|
329
301
|
except aiohttp.ClientError as e:
|
|
330
|
-
logger.exception(
|
|
331
|
-
|
|
332
|
-
)
|
|
333
|
-
raise UploadError(
|
|
334
|
-
f"HTTP error during video upload video_id={video_id}"
|
|
335
|
-
) from e
|
|
302
|
+
logger.exception("HTTP error during video upload video_id=%s", video_id)
|
|
303
|
+
raise UploadError(f"HTTP error during video upload video_id={video_id}") from e
|
|
336
304
|
except asyncio.TimeoutError as e:
|
|
337
|
-
logger.exception(
|
|
338
|
-
|
|
339
|
-
)
|
|
340
|
-
raise UploadError(
|
|
341
|
-
f"Timed out during video upload video_id={video_id}"
|
|
342
|
-
) from e
|
|
305
|
+
logger.exception("Timed out during video upload video_id=%s", video_id)
|
|
306
|
+
raise UploadError(f"Timed out during video upload video_id={video_id}") from e
|
|
343
307
|
except Exception as e:
|
|
344
|
-
logger.exception(
|
|
345
|
-
|
|
346
|
-
)
|
|
347
|
-
raise UploadError(
|
|
348
|
-
f"Unexpected error during video upload video_id={video_id}"
|
|
349
|
-
) from e
|
|
308
|
+
logger.exception("Unexpected error during video upload video_id=%s", video_id)
|
|
309
|
+
raise UploadError(f"Unexpected error during video upload video_id={video_id}") from e
|
|
350
310
|
finally:
|
|
351
311
|
self.video_upload_waiters.pop(video_id, None)
|
|
352
312
|
logger.debug("Video upload waiter removed video_id=%s", video_id)
|
|
@@ -458,70 +418,44 @@ class UploadService:
|
|
|
458
418
|
except UploadError:
|
|
459
419
|
raise
|
|
460
420
|
except aiohttp.ClientError as e:
|
|
461
|
-
logger.exception(
|
|
462
|
-
|
|
463
|
-
)
|
|
464
|
-
raise UploadError(
|
|
465
|
-
f"HTTP error during file upload file_id={file_id}"
|
|
466
|
-
) from e
|
|
421
|
+
logger.exception("HTTP error during file upload file_id=%s", file_id)
|
|
422
|
+
raise UploadError(f"HTTP error during file upload file_id={file_id}") from e
|
|
467
423
|
except asyncio.TimeoutError as e:
|
|
468
|
-
logger.exception(
|
|
469
|
-
|
|
470
|
-
)
|
|
471
|
-
raise UploadError(
|
|
472
|
-
f"Timed out during file upload file_id={file_id}"
|
|
473
|
-
) from e
|
|
424
|
+
logger.exception("Timed out during file upload file_id=%s", file_id)
|
|
425
|
+
raise UploadError(f"Timed out during file upload file_id={file_id}") from e
|
|
474
426
|
except Exception as e:
|
|
475
|
-
logger.exception(
|
|
476
|
-
|
|
477
|
-
)
|
|
478
|
-
raise UploadError(
|
|
479
|
-
f"Unexpected error during file upload file_id={file_id}"
|
|
480
|
-
) from e
|
|
427
|
+
logger.exception("Unexpected error during file upload file_id=%s", file_id)
|
|
428
|
+
raise UploadError(f"Unexpected error during file upload file_id={file_id}") from e
|
|
481
429
|
finally:
|
|
482
430
|
self.file_upload_waiters.pop(file_id, None)
|
|
483
431
|
logger.debug("File upload waiter removed file=%s", file_id)
|
|
484
432
|
|
|
485
|
-
async def on_video_attach(
|
|
486
|
-
self, attach: VideoUploadSignal, _: Client
|
|
487
|
-
) -> None:
|
|
433
|
+
async def on_video_attach(self, attach: VideoUploadSignal, _: Client) -> None:
|
|
488
434
|
logger.debug("Received attach event video_id=%s", attach.video_id)
|
|
489
435
|
|
|
490
436
|
future = self.video_upload_waiters.pop(attach.video_id, None)
|
|
491
437
|
|
|
492
438
|
if not future:
|
|
493
|
-
logger.debug(
|
|
494
|
-
"No video upload waiter found video_id=%s", attach.video_id
|
|
495
|
-
)
|
|
439
|
+
logger.debug("No video upload waiter found video_id=%s", attach.video_id)
|
|
496
440
|
return
|
|
497
441
|
|
|
498
442
|
if future.done():
|
|
499
|
-
logger.debug(
|
|
500
|
-
"Video upload waiter already done video_id=%s", attach.video_id
|
|
501
|
-
)
|
|
443
|
+
logger.debug("Video upload waiter already done video_id=%s", attach.video_id)
|
|
502
444
|
return
|
|
503
445
|
|
|
504
446
|
future.set_result(attach)
|
|
505
|
-
logger.debug(
|
|
506
|
-
"Video upload waiter resolved video_id=%s", attach.video_id
|
|
507
|
-
)
|
|
447
|
+
logger.debug("Video upload waiter resolved video_id=%s", attach.video_id)
|
|
508
448
|
|
|
509
|
-
async def on_file_attach(
|
|
510
|
-
self, attach: FileUploadSignal, _: Client
|
|
511
|
-
) -> None:
|
|
449
|
+
async def on_file_attach(self, attach: FileUploadSignal, _: Client) -> None:
|
|
512
450
|
logger.debug("Received attach event file_id=%s", attach.file_id)
|
|
513
451
|
future = self.file_upload_waiters.pop(attach.file_id, None)
|
|
514
452
|
|
|
515
453
|
if not future:
|
|
516
|
-
logger.debug(
|
|
517
|
-
"No file upload waiter found file_id=%s", attach.file_id
|
|
518
|
-
)
|
|
454
|
+
logger.debug("No file upload waiter found file_id=%s", attach.file_id)
|
|
519
455
|
return
|
|
520
456
|
|
|
521
457
|
if future.done():
|
|
522
|
-
logger.debug(
|
|
523
|
-
"File upload waiter already done file_id=%s", attach.file_id
|
|
524
|
-
)
|
|
458
|
+
logger.debug("File upload waiter already done file_id=%s", attach.file_id)
|
|
525
459
|
return
|
|
526
460
|
|
|
527
461
|
future.set_result(attach)
|
pymax/api/users/service.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING, Literal
|
|
4
4
|
|
|
5
|
+
from pymax.api.binding import bind_api_model
|
|
5
6
|
from pymax.api.response import (
|
|
6
7
|
parse_payload_list,
|
|
7
8
|
require_payload_dict,
|
|
@@ -30,13 +31,14 @@ class UserService:
|
|
|
30
31
|
self.app = app
|
|
31
32
|
|
|
32
33
|
def _cache_user(self, user: User) -> User:
|
|
34
|
+
user = bind_api_model(self.app, user)
|
|
33
35
|
self.app.users[user.id] = user
|
|
34
36
|
return user
|
|
35
37
|
|
|
36
38
|
def get_cached_user(self, user_id: int) -> User | None:
|
|
37
39
|
user = self.app.users.get(user_id)
|
|
38
40
|
logger.debug("get_cached_user id=%s hit=%s", user_id, bool(user))
|
|
39
|
-
return user
|
|
41
|
+
return bind_api_model(self.app, user) if user is not None else None
|
|
40
42
|
|
|
41
43
|
async def get_users(self, user_ids: list[int]) -> list[User]:
|
|
42
44
|
cached = {
|
|
@@ -44,9 +46,7 @@ class UserService:
|
|
|
44
46
|
for user_id in user_ids
|
|
45
47
|
if (user := self.get_cached_user(user_id)) is not None
|
|
46
48
|
}
|
|
47
|
-
missing_ids = [
|
|
48
|
-
user_id for user_id in user_ids if user_id not in cached
|
|
49
|
-
]
|
|
49
|
+
missing_ids = [user_id for user_id in user_ids if user_id not in cached]
|
|
50
50
|
|
|
51
51
|
if missing_ids:
|
|
52
52
|
for user in await self.fetch_users(missing_ids):
|
|
@@ -64,15 +64,11 @@ class UserService:
|
|
|
64
64
|
async def fetch_users(self, user_ids: list[int]) -> list[User]:
|
|
65
65
|
logger.info("fetching users count=%s", len(user_ids))
|
|
66
66
|
frame = FetchContactsPayload(contact_ids=user_ids)
|
|
67
|
-
response = await self.app.invoke(
|
|
68
|
-
Opcode.CONTACT_INFO, frame.to_payload()
|
|
69
|
-
)
|
|
67
|
+
response = await self.app.invoke(Opcode.CONTACT_INFO, frame.to_payload())
|
|
70
68
|
|
|
71
69
|
users = [
|
|
72
70
|
self._cache_user(user)
|
|
73
|
-
for user in parse_payload_list(
|
|
74
|
-
response, UserPayloadKey.CONTACTS, User
|
|
75
|
-
)
|
|
71
|
+
for user in parse_payload_list(response, UserPayloadKey.CONTACTS, User)
|
|
76
72
|
]
|
|
77
73
|
logger.debug("fetched users count=%s", len(users))
|
|
78
74
|
return users
|
|
@@ -97,12 +93,8 @@ class UserService:
|
|
|
97
93
|
response = await self.app.invoke(Opcode.SESSIONS_INFO, {})
|
|
98
94
|
return parse_payload_list(response, UserPayloadKey.SESSIONS, Session)
|
|
99
95
|
|
|
100
|
-
async def _contact_action(
|
|
101
|
-
self, payload
|
|
102
|
-
) -> InboundFrame:
|
|
103
|
-
response = await self.app.invoke(
|
|
104
|
-
Opcode.CONTACT_UPDATE, payload.to_payload()
|
|
105
|
-
)
|
|
96
|
+
async def _contact_action(self, payload: ContactActionPayload) -> InboundFrame:
|
|
97
|
+
response = await self.app.invoke(Opcode.CONTACT_UPDATE, payload.to_payload())
|
|
106
98
|
require_payload_dict(response)
|
|
107
99
|
return response
|
|
108
100
|
|
pymax/app.py
CHANGED
|
@@ -205,8 +205,10 @@ class App(Generic[ClientT]):
|
|
|
205
205
|
payload_keys,
|
|
206
206
|
)
|
|
207
207
|
logger.debug("Request data=%s", frame.model_dump())
|
|
208
|
+
|
|
208
209
|
request_timeout = self.config.request_timeout if timeout is None else timeout
|
|
209
210
|
response = await self.connection.request(frame, timeout=request_timeout)
|
|
211
|
+
|
|
210
212
|
response_keys = sorted(response.payload.keys()) if response.payload else []
|
|
211
213
|
logger.debug(
|
|
212
214
|
"response opcode=%s cmd=%s seq=%s payload_keys=%s",
|
pymax/auth/qr.py
CHANGED
|
@@ -118,23 +118,17 @@ class QrAuthFlow(AuthFlow):
|
|
|
118
118
|
continue
|
|
119
119
|
|
|
120
120
|
try:
|
|
121
|
-
response = await app.api.auth.check_password(
|
|
122
|
-
track_id, password
|
|
123
|
-
)
|
|
121
|
+
response = await app.api.auth.check_password(track_id, password)
|
|
124
122
|
except ApiError as e:
|
|
125
123
|
logger.error("2fa password check failed: %s", e)
|
|
126
124
|
continue
|
|
127
125
|
|
|
128
126
|
if response.error:
|
|
129
|
-
logger.error(
|
|
130
|
-
"2fa password check failed error=%s", response.error
|
|
131
|
-
)
|
|
127
|
+
logger.error("2fa password check failed error=%s", response.error)
|
|
132
128
|
continue
|
|
133
129
|
|
|
134
130
|
if response.login_token:
|
|
135
131
|
logger.info("2fa password authentication completed")
|
|
136
132
|
return response.login_token
|
|
137
133
|
|
|
138
|
-
logger.error(
|
|
139
|
-
"2fa password response did not contain login token; retrying"
|
|
140
|
-
)
|
|
134
|
+
logger.error("2fa password response did not contain login token; retrying")
|
pymax/auth/sms.py
CHANGED
|
@@ -75,13 +75,31 @@ class SmsAuthFlow(AuthFlow):
|
|
|
75
75
|
logger.debug("sms code provider returned code_set=%s", bool(code))
|
|
76
76
|
result = await app.api.auth.send_code(start.token, code)
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
if result.login_token:
|
|
79
|
+
token = result.login_token
|
|
80
|
+
elif not result.login_token and result.password_challenge:
|
|
80
81
|
token = await self._authenticate_with_password(
|
|
81
82
|
app,
|
|
82
83
|
track_id=result.password_challenge.track_id,
|
|
83
84
|
hint=result.password_challenge.hint,
|
|
84
85
|
)
|
|
86
|
+
elif result.register_token:
|
|
87
|
+
if not app.config.registration_config:
|
|
88
|
+
raise RuntimeError("RegistrationConfig is required to register a new account")
|
|
89
|
+
else:
|
|
90
|
+
registration_config = app.config.registration_config
|
|
91
|
+
response = await app.api.auth.confirm_registration(
|
|
92
|
+
first_name=registration_config.first_name,
|
|
93
|
+
last_name=registration_config.last_name,
|
|
94
|
+
token=result.register_token,
|
|
95
|
+
)
|
|
96
|
+
token = response.token
|
|
97
|
+
else:
|
|
98
|
+
logger.error(
|
|
99
|
+
"Authentication failed: server returned no login token, "
|
|
100
|
+
"password challenge, or registration token"
|
|
101
|
+
)
|
|
102
|
+
token = None
|
|
85
103
|
|
|
86
104
|
logger.info(
|
|
87
105
|
"sms authentication completed token_set=%s",
|
|
@@ -109,23 +127,17 @@ class SmsAuthFlow(AuthFlow):
|
|
|
109
127
|
continue
|
|
110
128
|
|
|
111
129
|
try:
|
|
112
|
-
response = await app.api.auth.check_password(
|
|
113
|
-
track_id, password
|
|
114
|
-
)
|
|
130
|
+
response = await app.api.auth.check_password(track_id, password)
|
|
115
131
|
except ApiError as e:
|
|
116
132
|
logger.error("2fa password check failed: %s", e)
|
|
117
133
|
continue
|
|
118
134
|
|
|
119
135
|
if response.error:
|
|
120
|
-
logger.error(
|
|
121
|
-
"2fa password check failed error=%s", response.error
|
|
122
|
-
)
|
|
136
|
+
logger.error("2fa password check failed error=%s", response.error)
|
|
123
137
|
continue
|
|
124
138
|
|
|
125
139
|
if response.login_token:
|
|
126
140
|
logger.info("2fa password authentication completed")
|
|
127
141
|
return response.login_token
|
|
128
142
|
|
|
129
|
-
logger.error(
|
|
130
|
-
"2fa password response did not contain login token; retrying"
|
|
131
|
-
)
|
|
143
|
+
logger.error("2fa password response did not contain login token; retrying")
|
pymax/base.py
CHANGED
|
@@ -22,7 +22,15 @@ if TYPE_CHECKING:
|
|
|
22
22
|
StartDecorator,
|
|
23
23
|
)
|
|
24
24
|
from pymax.protocol import InboundFrame
|
|
25
|
-
from pymax.types import
|
|
25
|
+
from pymax.types import (
|
|
26
|
+
Chat,
|
|
27
|
+
MessageDeleteEvent,
|
|
28
|
+
MessageReadEvent,
|
|
29
|
+
PresenceEvent,
|
|
30
|
+
ReactionUpdateEvent,
|
|
31
|
+
TypingEvent,
|
|
32
|
+
User,
|
|
33
|
+
)
|
|
26
34
|
from pymax.types.domain import Message, Profile
|
|
27
35
|
|
|
28
36
|
logger = get_logger(__name__)
|
|
@@ -83,6 +91,7 @@ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
|
|
|
83
91
|
sync=self.extra_config.sync,
|
|
84
92
|
store=self.extra_config.store,
|
|
85
93
|
proxy=self.extra_config.proxy,
|
|
94
|
+
registration_config=self.extra_config.registration_config,
|
|
86
95
|
device=DeviceConfig(
|
|
87
96
|
mt_instance_id=self.extra_config.mt_instance_id,
|
|
88
97
|
device_id=self.extra_config.device_id or str(uuid4()),
|
|
@@ -186,6 +195,34 @@ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
|
|
|
186
195
|
"""Регистрирует обработчик удаления сообщений."""
|
|
187
196
|
return self._router.on_message_delete(*filters)
|
|
188
197
|
|
|
198
|
+
def on_message_read(
|
|
199
|
+
self,
|
|
200
|
+
*filters: FilterCallback[MessageReadEvent],
|
|
201
|
+
) -> HandlerDecorator[MessageReadEvent, ClientT]:
|
|
202
|
+
"""Регистрирует обработчик изменения отметки прочтения."""
|
|
203
|
+
return self._router.on_message_read(*filters)
|
|
204
|
+
|
|
205
|
+
def on_typing(
|
|
206
|
+
self,
|
|
207
|
+
*filters: FilterCallback[TypingEvent],
|
|
208
|
+
) -> HandlerDecorator[TypingEvent, ClientT]:
|
|
209
|
+
"""Регистрирует обработчик набора текста."""
|
|
210
|
+
return self._router.on_typing(*filters)
|
|
211
|
+
|
|
212
|
+
def on_presence(
|
|
213
|
+
self,
|
|
214
|
+
*filters: FilterCallback[PresenceEvent],
|
|
215
|
+
) -> HandlerDecorator[PresenceEvent, ClientT]:
|
|
216
|
+
"""Регистрирует обработчик изменения присутствия пользователя."""
|
|
217
|
+
return self._router.on_presence(*filters)
|
|
218
|
+
|
|
219
|
+
def on_reaction_update(
|
|
220
|
+
self,
|
|
221
|
+
*filters: FilterCallback[ReactionUpdateEvent],
|
|
222
|
+
) -> HandlerDecorator[ReactionUpdateEvent, ClientT]:
|
|
223
|
+
"""Регистрирует обработчик обновления реакций сообщения."""
|
|
224
|
+
return self._router.on_reaction_update(*filters)
|
|
225
|
+
|
|
189
226
|
def on_chat_update(
|
|
190
227
|
self,
|
|
191
228
|
*filters: FilterCallback[Chat],
|
pymax/client.py
CHANGED
|
@@ -39,7 +39,7 @@ class Client(BaseClient["Client"]):
|
|
|
39
39
|
password_provider: Провайдер пароля 2FA, если аккаунт его требует.
|
|
40
40
|
"""
|
|
41
41
|
|
|
42
|
-
def __init__(
|
|
42
|
+
def __init__( # noqa: PLR0913
|
|
43
43
|
self,
|
|
44
44
|
phone: str,
|
|
45
45
|
session_name: str = "session.db",
|
|
@@ -49,6 +49,7 @@ class Client(BaseClient["Client"]):
|
|
|
49
49
|
sms_code_provider: SmsCodeProvider | None = None,
|
|
50
50
|
password_provider: PasswordProvider | None = None,
|
|
51
51
|
) -> None:
|
|
52
|
+
|
|
52
53
|
self.phone = phone
|
|
53
54
|
self.extra_config = extra_config or ExtraConfig()
|
|
54
55
|
self.session_name = session_name
|
pymax/client_web.py
CHANGED
|
@@ -54,8 +54,7 @@ class WebClient(BaseClient["WebClient"]):
|
|
|
54
54
|
self._config = self._build_config(
|
|
55
55
|
phone=None,
|
|
56
56
|
user_agent=(
|
|
57
|
-
self.extra_config.user_agent
|
|
58
|
-
or self.extra_config.generate_web_user_agent()
|
|
57
|
+
self.extra_config.user_agent or self.extra_config.generate_web_user_agent()
|
|
59
58
|
),
|
|
60
59
|
)
|
|
61
60
|
|
pymax/config.py
CHANGED
|
@@ -84,6 +84,21 @@ class DeviceConfig(BaseModel):
|
|
|
84
84
|
client_session_id: int = Field(default_factory=lambda: randint(1, 70))
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
class RegistrationConfig(BaseModel):
|
|
88
|
+
"""Данные профиля для регистрации нового аккаунта по SMS.
|
|
89
|
+
|
|
90
|
+
Передайте объект через ``ExtraConfig.registration_config``. Он используется
|
|
91
|
+
только если после подтверждения SMS-кода Max вернул токен регистрации.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
first_name: Имя нового пользователя.
|
|
95
|
+
last_name: Фамилия нового пользователя.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
first_name: str
|
|
99
|
+
last_name: str | None = None
|
|
100
|
+
|
|
101
|
+
|
|
87
102
|
class ClientConfig(BaseModel):
|
|
88
103
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
89
104
|
|
|
@@ -93,6 +108,7 @@ class ClientConfig(BaseModel):
|
|
|
93
108
|
device: DeviceConfig
|
|
94
109
|
token: str | None = None
|
|
95
110
|
proxy: str | None = None
|
|
111
|
+
registration_config: RegistrationConfig | None = None
|
|
96
112
|
|
|
97
113
|
host: str = "api.oneme.ru"
|
|
98
114
|
port: int = 443
|
|
@@ -109,9 +125,7 @@ class ClientConfig(BaseModel):
|
|
|
109
125
|
|
|
110
126
|
def ensure_config(self) -> None:
|
|
111
127
|
if not self.phone:
|
|
112
|
-
raise ValueError(
|
|
113
|
-
"Phone must be provided when no saved session exists."
|
|
114
|
-
)
|
|
128
|
+
raise ValueError("Phone must be provided when no saved session exists.")
|
|
115
129
|
|
|
116
130
|
|
|
117
131
|
class ExtraConfig(BaseModel):
|
|
@@ -122,6 +136,8 @@ class ExtraConfig(BaseModel):
|
|
|
122
136
|
|
|
123
137
|
Args:
|
|
124
138
|
token: Готовый token для создания сессии без SMS/QR.
|
|
139
|
+
registration_config: Имя и фамилия для автоматического завершения
|
|
140
|
+
регистрации нового аккаунта по SMS.
|
|
125
141
|
host: TCP host Max API.
|
|
126
142
|
port: TCP port Max API.
|
|
127
143
|
url: WebSocket URL для ``WebClient``.
|
|
@@ -156,6 +172,7 @@ class ExtraConfig(BaseModel):
|
|
|
156
172
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
157
173
|
|
|
158
174
|
token: str | None = None
|
|
175
|
+
registration_config: RegistrationConfig | None = None
|
|
159
176
|
|
|
160
177
|
host: str = "api.oneme.ru"
|
|
161
178
|
port: int = 443
|
|
@@ -221,3 +238,25 @@ class ExtraConfig(BaseModel):
|
|
|
221
238
|
device_locale=locale,
|
|
222
239
|
header_user_agent=DEFAULT_WEB_HEADER_USER_AGENT,
|
|
223
240
|
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ignore. for future upd
|
|
244
|
+
|
|
245
|
+
# class TcpOptions(BaseModel):
|
|
246
|
+
# host: str = "api.oneme.ru"
|
|
247
|
+
# port: int = 443
|
|
248
|
+
# use_ssl: bool = True
|
|
249
|
+
# proxy: str | None = None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# class RuntimeOptions(BaseModel):
|
|
253
|
+
# request_timeout: float = 30.0
|
|
254
|
+
# reconnect: bool = True
|
|
255
|
+
# reconnect_delay: float = 1.0
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# class DeviceOptions(BaseModel):
|
|
259
|
+
# device_id: str | None = None
|
|
260
|
+
# device_type: DeviceType = DeviceType.ANDROID
|
|
261
|
+
# user_agent: MobileUserAgentPayload | None = None
|
|
262
|
+
# mt_instance_id: str = Field(default_factory=lambda: str(uuid4()))
|
pymax/connection/connection.py
CHANGED
|
@@ -183,7 +183,9 @@ class ConnectionManager:
|
|
|
183
183
|
except Exception as e:
|
|
184
184
|
exc = ConnectionError(f"Connection error: {e}")
|
|
185
185
|
logger.exception("connection receive loop failed")
|
|
186
|
+
|
|
186
187
|
self.requests.cancel_all(exc=exc)
|
|
188
|
+
|
|
187
189
|
self._connection_lost = True
|
|
188
190
|
self._mark_closed(exc)
|
|
189
191
|
raise e
|
pymax/connection/readers/tcp.py
CHANGED
|
@@ -8,9 +8,7 @@ logger = get_logger(__name__)
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class TCPReader(BaseReader):
|
|
11
|
-
def __init__(
|
|
12
|
-
self, transport: TCPTransport, framer: TcpPacketFramer
|
|
13
|
-
) -> None:
|
|
11
|
+
def __init__(self, transport: TCPTransport, framer: TcpPacketFramer) -> None:
|
|
14
12
|
super().__init__()
|
|
15
13
|
self.transport = transport
|
|
16
14
|
self.framer = framer
|