maxapi-python 1.2.4__py3-none-any.whl → 2.0.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.0.0.dist-info/METADATA +217 -0
- maxapi_python-2.0.0.dist-info/RECORD +140 -0
- {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/WHEEL +1 -1
- pymax/__init__.py +50 -105
- pymax/api/__init__.py +17 -0
- pymax/api/auth/__init__.py +1 -0
- pymax/api/auth/enums.py +17 -0
- pymax/api/auth/payloads.py +129 -0
- pymax/api/auth/service.py +313 -0
- pymax/api/auth/types.py +13 -0
- pymax/api/chats/__init__.py +8 -0
- pymax/api/chats/enums.py +27 -0
- pymax/api/chats/payloads.py +103 -0
- pymax/api/chats/service.py +277 -0
- pymax/api/facade.py +32 -0
- pymax/api/messages/__init__.py +1 -0
- pymax/api/messages/enums.py +17 -0
- pymax/api/messages/payloads.py +92 -0
- pymax/api/messages/service.py +337 -0
- pymax/api/models.py +13 -0
- pymax/api/response.py +123 -0
- pymax/api/self/__init__.py +2 -0
- pymax/api/self/enums.py +11 -0
- pymax/api/self/payloads.py +41 -0
- pymax/api/self/service.py +142 -0
- pymax/api/session/__init__.py +1 -0
- pymax/api/session/enums.py +10 -0
- pymax/api/session/payloads.py +76 -0
- pymax/api/session/service.py +72 -0
- pymax/api/uploads/__init__.py +1 -0
- pymax/api/uploads/models.py +49 -0
- pymax/api/uploads/payloads.py +25 -0
- pymax/api/uploads/service.py +458 -0
- pymax/api/users/__init__.py +2 -0
- pymax/api/users/enums.py +12 -0
- pymax/api/users/payloads.py +16 -0
- pymax/api/users/service.py +124 -0
- pymax/app.py +273 -0
- pymax/auth/__init__.py +25 -0
- pymax/auth/base.py +37 -0
- pymax/auth/email.py +0 -0
- pymax/auth/models.py +5 -0
- pymax/auth/providers.py +127 -0
- pymax/auth/qr.py +135 -0
- pymax/auth/service.py +25 -0
- pymax/auth/sms.py +122 -0
- pymax/base.py +204 -0
- pymax/client.py +106 -0
- pymax/client_web.py +83 -0
- pymax/config.py +215 -0
- pymax/connection/__init__.py +1 -0
- pymax/connection/connection.py +205 -0
- pymax/connection/pending.py +46 -0
- pymax/connection/readers/__init__.py +2 -0
- pymax/connection/readers/base.py +6 -0
- pymax/connection/readers/tcp.py +29 -0
- pymax/connection/readers/ws.py +14 -0
- pymax/dispatch/__init__.py +10 -0
- pymax/dispatch/dispatcher.py +222 -0
- pymax/dispatch/enums.py +12 -0
- pymax/dispatch/mapping.py +73 -0
- pymax/dispatch/resolvers.py +52 -0
- pymax/dispatch/router.py +216 -0
- pymax/exceptions.py +22 -89
- pymax/files/__init__.py +9 -0
- pymax/files/base.py +82 -0
- pymax/files/file.py +76 -0
- pymax/files/photo.py +108 -0
- pymax/files/static.py +10 -0
- pymax/files/video.py +74 -0
- pymax/formatting/__init__.py +0 -0
- pymax/formatting/markdown.py +217 -0
- pymax/infra/__init__.py +1 -0
- pymax/infra/auth.py +55 -0
- pymax/infra/base.py +15 -0
- pymax/infra/chat.py +240 -0
- pymax/infra/message.py +252 -0
- pymax/infra/protocol.py +9 -0
- pymax/infra/self.py +139 -0
- pymax/infra/user.py +107 -0
- pymax/logging.py +129 -0
- pymax/protocol/__init__.py +11 -0
- pymax/protocol/base.py +13 -0
- pymax/{static/enum.py → protocol/enums.py} +36 -79
- pymax/protocol/models.py +33 -0
- pymax/protocol/tcp/__init__.py +1 -0
- pymax/protocol/tcp/compression.py +97 -0
- pymax/protocol/tcp/framing.py +68 -0
- pymax/protocol/tcp/payload.py +127 -0
- pymax/protocol/tcp/protocol.py +68 -0
- pymax/protocol/ws/__init__.py +1 -0
- pymax/protocol/ws/protocol.py +27 -0
- pymax/py.typed +0 -0
- pymax/routers.py +8 -0
- pymax/session/__init__.py +3 -0
- pymax/session/models.py +11 -0
- pymax/session/protocol.py +14 -0
- pymax/session/store.py +232 -0
- pymax/telemetry/__init__.py +3 -0
- pymax/telemetry/navigation.py +181 -0
- pymax/telemetry/payloads.py +142 -0
- pymax/telemetry/service.py +225 -0
- pymax/transport/__init__.py +0 -0
- pymax/transport/base.py +14 -0
- pymax/transport/tcp.py +93 -0
- pymax/transport/websocket.py +50 -0
- pymax/types/__init__.py +2 -0
- pymax/types/domain/__init__.py +11 -0
- pymax/types/domain/attachments/__init__.py +11 -0
- pymax/types/domain/attachments/audio.py +35 -0
- pymax/types/domain/attachments/call.py +26 -0
- pymax/types/domain/attachments/contact.py +32 -0
- pymax/types/domain/attachments/control.py +20 -0
- pymax/types/domain/attachments/enums.py +27 -0
- pymax/types/domain/attachments/file.py +56 -0
- pymax/types/domain/attachments/keyboards/__init__.py +1 -0
- pymax/types/domain/attachments/keyboards/inline.py +19 -0
- pymax/types/domain/attachments/photo.py +45 -0
- pymax/types/domain/attachments/share.py +29 -0
- pymax/types/domain/attachments/sticker.py +50 -0
- pymax/types/domain/attachments/video.py +90 -0
- pymax/types/domain/auth.py +161 -0
- pymax/types/domain/base.py +17 -0
- pymax/types/domain/chat.py +426 -0
- pymax/types/domain/element.py +24 -0
- pymax/types/domain/enums.py +24 -0
- pymax/types/domain/error.py +20 -0
- pymax/types/domain/folder.py +74 -0
- pymax/types/domain/login.py +35 -0
- pymax/types/domain/message.py +378 -0
- pymax/types/domain/name.py +20 -0
- pymax/types/domain/profile.py +15 -0
- pymax/types/domain/session.py +52 -0
- pymax/types/domain/sync.py +80 -0
- pymax/types/domain/user.py +117 -0
- pymax/types/events/__init__.py +3 -0
- pymax/types/events/file.py +5 -0
- pymax/types/events/message.py +37 -0
- pymax/types/events/video.py +5 -0
- maxapi_python-1.2.4.dist-info/METADATA +0 -205
- maxapi_python-1.2.4.dist-info/RECORD +0 -33
- pymax/core.py +0 -390
- pymax/crud.py +0 -96
- pymax/files.py +0 -138
- pymax/filters.py +0 -164
- pymax/formatter.py +0 -31
- pymax/formatting.py +0 -74
- pymax/interfaces.py +0 -552
- pymax/mixins/__init__.py +0 -40
- pymax/mixins/auth.py +0 -368
- pymax/mixins/channel.py +0 -130
- pymax/mixins/group.py +0 -458
- pymax/mixins/handler.py +0 -285
- pymax/mixins/message.py +0 -879
- pymax/mixins/scheduler.py +0 -28
- pymax/mixins/self.py +0 -259
- pymax/mixins/socket.py +0 -297
- pymax/mixins/telemetry.py +0 -112
- pymax/mixins/user.py +0 -219
- pymax/mixins/websocket.py +0 -142
- pymax/models.py +0 -8
- pymax/navigation.py +0 -187
- pymax/payloads.py +0 -367
- pymax/protocols.py +0 -123
- pymax/static/constant.py +0 -89
- pymax/types.py +0 -1220
- pymax/utils.py +0 -90
- {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/licenses/LICENSE +0 -0
pymax/mixins/auth.py
DELETED
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import datetime
|
|
3
|
-
import re
|
|
4
|
-
import sys
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
import qrcode
|
|
8
|
-
|
|
9
|
-
from pymax.exceptions import Error
|
|
10
|
-
from pymax.payloads import RegisterPayload, RequestCodePayload, SendCodePayload
|
|
11
|
-
from pymax.protocols import ClientProtocol
|
|
12
|
-
from pymax.static.constant import PHONE_REGEX
|
|
13
|
-
from pymax.static.enum import AuthType, DeviceType, Opcode
|
|
14
|
-
from pymax.utils import MixinsUtils
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class AuthMixin(ClientProtocol):
|
|
18
|
-
def _check_phone(self) -> bool:
|
|
19
|
-
return bool(re.match(PHONE_REGEX, self.phone))
|
|
20
|
-
|
|
21
|
-
async def request_code(self, phone: str, language: str = "ru") -> str:
|
|
22
|
-
"""
|
|
23
|
-
Запрашивает код аутентификации для указанного номера телефона и возвращает временный токен.
|
|
24
|
-
|
|
25
|
-
Метод отправляет запрос на получение кода верификации на переданный номер телефона.
|
|
26
|
-
Используется в процессе аутентификации или регистрации.
|
|
27
|
-
|
|
28
|
-
:param phone: Номер телефона в международном формате.
|
|
29
|
-
:type phone: str
|
|
30
|
-
:param language: Язык для сообщения с кодом. По умолчанию "ru".
|
|
31
|
-
:type language: str
|
|
32
|
-
:return: Временный токен для дальнейшей аутентификации.
|
|
33
|
-
:rtype: str
|
|
34
|
-
:raises ValueError: Если полученные данные имеют неверный формат.
|
|
35
|
-
:raises Error: Если сервер вернул ошибку.
|
|
36
|
-
|
|
37
|
-
.. note::
|
|
38
|
-
Используется только в пользовательском flow аутентификации.
|
|
39
|
-
"""
|
|
40
|
-
self.logger.info("Requesting auth code")
|
|
41
|
-
|
|
42
|
-
payload = RequestCodePayload(
|
|
43
|
-
phone=phone, type=AuthType.START_AUTH, language=language
|
|
44
|
-
).model_dump(by_alias=True)
|
|
45
|
-
|
|
46
|
-
data = await self._send_and_wait(opcode=Opcode.AUTH_REQUEST, payload=payload)
|
|
47
|
-
|
|
48
|
-
if data.get("payload", {}).get("error"):
|
|
49
|
-
MixinsUtils.handle_error(data)
|
|
50
|
-
|
|
51
|
-
self.logger.debug(
|
|
52
|
-
"Code request response opcode=%s seq=%s",
|
|
53
|
-
data.get("opcode"),
|
|
54
|
-
data.get("seq"),
|
|
55
|
-
)
|
|
56
|
-
payload_data = data.get("payload")
|
|
57
|
-
if isinstance(payload_data, dict):
|
|
58
|
-
return payload_data["token"]
|
|
59
|
-
else:
|
|
60
|
-
self.logger.error("Invalid payload data received")
|
|
61
|
-
raise ValueError("Invalid payload data received")
|
|
62
|
-
|
|
63
|
-
async def resend_code(self, phone: str, language: str = "ru") -> str:
|
|
64
|
-
"""
|
|
65
|
-
Повторно запрашивает код аутентификации для указанного номера телефона и возвращает временный токен.
|
|
66
|
-
|
|
67
|
-
:param phone: Номер телефона в международном формате.
|
|
68
|
-
:type phone: str
|
|
69
|
-
:param language: Язык для сообщения с кодом. По умолчанию "ru".
|
|
70
|
-
:type language: str
|
|
71
|
-
:return: Временный токен для дальнейшей аутентификации.
|
|
72
|
-
:rtype: str
|
|
73
|
-
:raises ValueError: Если полученные данные имеют неверный формат.
|
|
74
|
-
:raises Error: Если сервер вернул ошибку.
|
|
75
|
-
"""
|
|
76
|
-
self.logger.info("Resending auth code")
|
|
77
|
-
|
|
78
|
-
payload = RequestCodePayload(
|
|
79
|
-
phone=phone, type=AuthType.RESEND, language=language
|
|
80
|
-
).model_dump(by_alias=True)
|
|
81
|
-
|
|
82
|
-
data = await self._send_and_wait(opcode=Opcode.AUTH_REQUEST, payload=payload)
|
|
83
|
-
|
|
84
|
-
if data.get("payload", {}).get("error"):
|
|
85
|
-
MixinsUtils.handle_error(data)
|
|
86
|
-
|
|
87
|
-
self.logger.debug(
|
|
88
|
-
"Code resend response opcode=%s seq=%s",
|
|
89
|
-
data.get("opcode"),
|
|
90
|
-
data.get("seq"),
|
|
91
|
-
)
|
|
92
|
-
payload_data = data.get("payload")
|
|
93
|
-
if isinstance(payload_data, dict):
|
|
94
|
-
return payload_data["token"]
|
|
95
|
-
else:
|
|
96
|
-
self.logger.error("Invalid payload data received")
|
|
97
|
-
raise ValueError("Invalid payload data received")
|
|
98
|
-
|
|
99
|
-
async def _send_code(self, code: str, token: str) -> dict[str, Any]:
|
|
100
|
-
"""
|
|
101
|
-
Отправляет код верификации на сервер для подтверждения.
|
|
102
|
-
|
|
103
|
-
:param code: Код верификации (6 цифр).
|
|
104
|
-
:type code: str
|
|
105
|
-
:param token: Временный токен, полученный из request_code.
|
|
106
|
-
:type token: str
|
|
107
|
-
:return: Словарь с данными ответа сервера, содержащий токены аутентификации.
|
|
108
|
-
:rtype: dict[str, Any]
|
|
109
|
-
:raises Error: Если сервер вернул ошибку.
|
|
110
|
-
"""
|
|
111
|
-
self.logger.info("Sending verification code")
|
|
112
|
-
|
|
113
|
-
payload = SendCodePayload(
|
|
114
|
-
token=token,
|
|
115
|
-
verify_code=code,
|
|
116
|
-
auth_token_type=AuthType.CHECK_CODE,
|
|
117
|
-
).model_dump(by_alias=True)
|
|
118
|
-
|
|
119
|
-
data = await self._send_and_wait(opcode=Opcode.AUTH, payload=payload)
|
|
120
|
-
|
|
121
|
-
if data.get("payload", {}).get("error"):
|
|
122
|
-
MixinsUtils.handle_error(data)
|
|
123
|
-
|
|
124
|
-
self.logger.debug(
|
|
125
|
-
"Send code response opcode=%s seq=%s",
|
|
126
|
-
data.get("opcode"),
|
|
127
|
-
data.get("seq"),
|
|
128
|
-
)
|
|
129
|
-
payload_data = data.get("payload")
|
|
130
|
-
if isinstance(payload_data, dict):
|
|
131
|
-
return payload_data
|
|
132
|
-
else:
|
|
133
|
-
self.logger.error("Invalid payload data received")
|
|
134
|
-
raise ValueError("Invalid payload data received")
|
|
135
|
-
|
|
136
|
-
def _print_qr(self, qr_link: str) -> None:
|
|
137
|
-
qr = qrcode.QRCode(
|
|
138
|
-
version=1,
|
|
139
|
-
error_correction=qrcode.ERROR_CORRECT_L,
|
|
140
|
-
box_size=1,
|
|
141
|
-
border=1,
|
|
142
|
-
)
|
|
143
|
-
qr.add_data(qr_link)
|
|
144
|
-
qr.make(fit=True)
|
|
145
|
-
|
|
146
|
-
qr.print_ascii()
|
|
147
|
-
|
|
148
|
-
async def _request_qr_login(self) -> dict[str, Any]:
|
|
149
|
-
self.logger.info("Requesting QR login data")
|
|
150
|
-
|
|
151
|
-
data = await self._send_and_wait(opcode=Opcode.GET_QR, payload={})
|
|
152
|
-
|
|
153
|
-
if data.get("payload", {}).get("error"):
|
|
154
|
-
MixinsUtils.handle_error(data)
|
|
155
|
-
|
|
156
|
-
self.logger.debug(
|
|
157
|
-
"QR login data response opcode=%s seq=%s",
|
|
158
|
-
data.get("opcode"),
|
|
159
|
-
data.get("seq"),
|
|
160
|
-
)
|
|
161
|
-
payload_data = data.get("payload")
|
|
162
|
-
if isinstance(payload_data, dict):
|
|
163
|
-
return payload_data
|
|
164
|
-
else:
|
|
165
|
-
self.logger.error("Invalid payload data received")
|
|
166
|
-
raise ValueError("Invalid payload data received")
|
|
167
|
-
|
|
168
|
-
def _validate_version(self, version: str, min_version: str) -> bool:
|
|
169
|
-
def version_tuple(v: str) -> tuple[int, ...]:
|
|
170
|
-
return tuple(map(int, (v.split("."))))
|
|
171
|
-
|
|
172
|
-
return version_tuple(version) >= version_tuple(min_version)
|
|
173
|
-
|
|
174
|
-
async def _login(self) -> None:
|
|
175
|
-
self.logger.info("Starting login flow")
|
|
176
|
-
|
|
177
|
-
if self.user_agent.device_type == DeviceType.WEB.value and self._ws:
|
|
178
|
-
if not self._validate_version(self.user_agent.app_version, "25.12.13"):
|
|
179
|
-
self.logger.error("Your app version is too old")
|
|
180
|
-
raise ValueError("Your app version is too old")
|
|
181
|
-
|
|
182
|
-
login_resp = await self._login_by_qr()
|
|
183
|
-
else:
|
|
184
|
-
temp_token = await self.request_code(self.phone)
|
|
185
|
-
if not temp_token or not isinstance(temp_token, str):
|
|
186
|
-
self.logger.critical("Failed to request code: token missing")
|
|
187
|
-
raise ValueError("Failed to request code")
|
|
188
|
-
|
|
189
|
-
print("Введите код: ", end="", flush=True)
|
|
190
|
-
code = await asyncio.to_thread(lambda: sys.stdin.readline().strip())
|
|
191
|
-
if len(code) != 6 or not code.isdigit():
|
|
192
|
-
self.logger.error("Invalid code format entered")
|
|
193
|
-
raise ValueError("Invalid code format")
|
|
194
|
-
|
|
195
|
-
login_resp = await self._send_code(code, temp_token)
|
|
196
|
-
|
|
197
|
-
token = login_resp.get("tokenAttrs", {}).get("LOGIN", {}).get("token", "")
|
|
198
|
-
|
|
199
|
-
if not token:
|
|
200
|
-
self.logger.critical("Failed to login, token not received")
|
|
201
|
-
raise ValueError("Failed to login, token not received")
|
|
202
|
-
|
|
203
|
-
self._token = token
|
|
204
|
-
self._database.update_auth_token((self._device_id), self._token)
|
|
205
|
-
self.logger.info("Login successful, token saved to database")
|
|
206
|
-
|
|
207
|
-
async def _poll_qr_login(self, track_id: str, poll_interval: int) -> bool:
|
|
208
|
-
self.logger.info("Polling for QR login confirmation")
|
|
209
|
-
|
|
210
|
-
while True:
|
|
211
|
-
data = await self._send_and_wait(
|
|
212
|
-
opcode=Opcode.GET_QR_STATUS,
|
|
213
|
-
payload={"trackId": track_id},
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
payload = data.get("payload", {})
|
|
217
|
-
|
|
218
|
-
if payload.get("error"):
|
|
219
|
-
MixinsUtils.handle_error(data)
|
|
220
|
-
status = payload.get("status")
|
|
221
|
-
|
|
222
|
-
if not status:
|
|
223
|
-
self.logger.warning("No status in QR login response")
|
|
224
|
-
continue
|
|
225
|
-
|
|
226
|
-
if status.get("loginAvailable"):
|
|
227
|
-
self.logger.info("QR login confirmed")
|
|
228
|
-
return True
|
|
229
|
-
else:
|
|
230
|
-
exp_at = status.get("expiresAt")
|
|
231
|
-
if (
|
|
232
|
-
exp_at
|
|
233
|
-
and isinstance(exp_at, (int, float))
|
|
234
|
-
and exp_at < datetime.datetime.now().timestamp() * 1000
|
|
235
|
-
):
|
|
236
|
-
self.logger.warning("QR code expired")
|
|
237
|
-
return False
|
|
238
|
-
|
|
239
|
-
await asyncio.sleep(poll_interval / 1000)
|
|
240
|
-
|
|
241
|
-
async def _get_qr_login_data(self, track_id: str) -> dict[str, Any]:
|
|
242
|
-
self.logger.info("Getting QR login data")
|
|
243
|
-
|
|
244
|
-
data = await self._send_and_wait(
|
|
245
|
-
opcode=Opcode.LOGIN_BY_QR,
|
|
246
|
-
payload={"trackId": track_id},
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
self.logger.debug(
|
|
250
|
-
"QR login data response opcode=%s seq=%s",
|
|
251
|
-
data.get("opcode"),
|
|
252
|
-
data.get("seq"),
|
|
253
|
-
)
|
|
254
|
-
payload_data = data.get("payload")
|
|
255
|
-
if isinstance(payload_data, dict):
|
|
256
|
-
return payload_data
|
|
257
|
-
else:
|
|
258
|
-
self.logger.error("Invalid payload data received")
|
|
259
|
-
raise ValueError("Invalid payload data received")
|
|
260
|
-
|
|
261
|
-
async def _login_by_qr(self) -> dict[str, Any]:
|
|
262
|
-
data = await self._request_qr_login()
|
|
263
|
-
|
|
264
|
-
poll_interval = data.get("pollingInterval")
|
|
265
|
-
link = data.get("qrLink")
|
|
266
|
-
track_id = data.get("trackId")
|
|
267
|
-
expires_at = data.get("expiresAt")
|
|
268
|
-
|
|
269
|
-
if not poll_interval or not link or not track_id or not expires_at:
|
|
270
|
-
self.logger.critical("Invalid QR login data received")
|
|
271
|
-
raise ValueError("Invalid QR login data received")
|
|
272
|
-
|
|
273
|
-
self.logger.info("Starting QR login flow")
|
|
274
|
-
self._print_qr(link)
|
|
275
|
-
|
|
276
|
-
poll_qr_task = asyncio.create_task(self._poll_qr_login(track_id, poll_interval))
|
|
277
|
-
|
|
278
|
-
while True:
|
|
279
|
-
now_ms = datetime.datetime.now().timestamp() * 1000
|
|
280
|
-
|
|
281
|
-
done, pending = await asyncio.wait(
|
|
282
|
-
[poll_qr_task],
|
|
283
|
-
timeout=1,
|
|
284
|
-
return_when=asyncio.FIRST_COMPLETED,
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
if now_ms >= expires_at:
|
|
288
|
-
poll_qr_task.cancel()
|
|
289
|
-
self.logger.error("QR code expired before confirmation")
|
|
290
|
-
raise RuntimeError("QR code expired before confirmation")
|
|
291
|
-
|
|
292
|
-
if poll_qr_task in done:
|
|
293
|
-
exc = poll_qr_task.exception()
|
|
294
|
-
if exc is not None:
|
|
295
|
-
raise exc
|
|
296
|
-
elif poll_qr_task.result():
|
|
297
|
-
self.logger.info("QR login successful")
|
|
298
|
-
|
|
299
|
-
data = await self._get_qr_login_data(track_id)
|
|
300
|
-
return data
|
|
301
|
-
|
|
302
|
-
else:
|
|
303
|
-
self.logger.error("QR login failed or expired")
|
|
304
|
-
raise RuntimeError("QR login failed or expired")
|
|
305
|
-
|
|
306
|
-
async def _submit_reg_info(
|
|
307
|
-
self, first_name: str, last_name: str | None, token: str
|
|
308
|
-
) -> dict[str, Any]:
|
|
309
|
-
try:
|
|
310
|
-
self.logger.info("Submitting registration info")
|
|
311
|
-
|
|
312
|
-
payload = RegisterPayload(
|
|
313
|
-
first_name=first_name,
|
|
314
|
-
last_name=last_name,
|
|
315
|
-
token=token,
|
|
316
|
-
).model_dump(by_alias=True)
|
|
317
|
-
|
|
318
|
-
data = await self._send_and_wait(opcode=Opcode.AUTH_CONFIRM, payload=payload)
|
|
319
|
-
if data.get("payload", {}).get("error"):
|
|
320
|
-
MixinsUtils.handle_error(data)
|
|
321
|
-
|
|
322
|
-
self.logger.debug(
|
|
323
|
-
"Registration info response opcode=%s seq=%s",
|
|
324
|
-
data.get("opcode"),
|
|
325
|
-
data.get("seq"),
|
|
326
|
-
)
|
|
327
|
-
payload_data = data.get("payload")
|
|
328
|
-
if isinstance(payload_data, dict):
|
|
329
|
-
return payload_data
|
|
330
|
-
raise ValueError("Invalid payload data received")
|
|
331
|
-
except Exception:
|
|
332
|
-
self.logger.error("Submit registration info failed", exc_info=True)
|
|
333
|
-
raise RuntimeError("Submit registration info failed")
|
|
334
|
-
|
|
335
|
-
async def _register(self, first_name: str, last_name: str | None = None) -> None:
|
|
336
|
-
self.logger.info("Starting registration flow")
|
|
337
|
-
|
|
338
|
-
request_code_payload = await self.request_code(self.phone)
|
|
339
|
-
temp_token = request_code_payload
|
|
340
|
-
|
|
341
|
-
if not temp_token or not isinstance(temp_token, str):
|
|
342
|
-
self.logger.critical("Failed to request code: token missing")
|
|
343
|
-
raise ValueError("Failed to request code")
|
|
344
|
-
|
|
345
|
-
print("Введите код: ", end="", flush=True)
|
|
346
|
-
code = await asyncio.to_thread(lambda: sys.stdin.readline().strip())
|
|
347
|
-
if len(code) != 6 or not code.isdigit():
|
|
348
|
-
self.logger.error("Invalid code format entered")
|
|
349
|
-
raise ValueError("Invalid code format")
|
|
350
|
-
|
|
351
|
-
registration_response = await self._send_code(code, temp_token)
|
|
352
|
-
token = registration_response.get("tokenAttrs", {}).get("REGISTER", {}).get("token", "")
|
|
353
|
-
if not token:
|
|
354
|
-
self.logger.critical("Failed to register, token not received")
|
|
355
|
-
raise ValueError("Failed to register, token not received")
|
|
356
|
-
|
|
357
|
-
data = await self._submit_reg_info(first_name, last_name, token)
|
|
358
|
-
self._token = data.get("token")
|
|
359
|
-
if not self._token:
|
|
360
|
-
self.logger.critical("Failed to register, token not received")
|
|
361
|
-
raise ValueError("Failed to register, token not received")
|
|
362
|
-
|
|
363
|
-
self.logger.info("Registration successful")
|
|
364
|
-
self.logger.info("Token: %s", self._token)
|
|
365
|
-
self.logger.warning(
|
|
366
|
-
"IMPORTANT: Use this token ONLY with device_type='DESKTOP' and the special init user agent"
|
|
367
|
-
)
|
|
368
|
-
self.logger.warning("This token MUST NOT be used in web clients")
|
pymax/mixins/channel.py
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
from pymax.exceptions import Error, ResponseError, ResponseStructureError
|
|
2
|
-
from pymax.payloads import (
|
|
3
|
-
GetGroupMembersPayload,
|
|
4
|
-
JoinChatPayload,
|
|
5
|
-
ResolveLinkPayload,
|
|
6
|
-
SearchGroupMembersPayload,
|
|
7
|
-
)
|
|
8
|
-
from pymax.protocols import ClientProtocol
|
|
9
|
-
from pymax.static.constant import (
|
|
10
|
-
DEFAULT_CHAT_MEMBERS_LIMIT,
|
|
11
|
-
DEFAULT_MARKER_VALUE,
|
|
12
|
-
)
|
|
13
|
-
from pymax.static.enum import Opcode
|
|
14
|
-
from pymax.types import Channel, Member
|
|
15
|
-
from pymax.utils import MixinsUtils
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class ChannelMixin(ClientProtocol):
|
|
19
|
-
async def resolve_channel_by_name(self, name: str) -> Channel | None:
|
|
20
|
-
"""
|
|
21
|
-
Получает информацию о канале по его имени
|
|
22
|
-
|
|
23
|
-
:param name: Имя канала
|
|
24
|
-
:type name: str
|
|
25
|
-
:return: Объект Channel или None, если канал не найден
|
|
26
|
-
:rtype: Channel | None
|
|
27
|
-
"""
|
|
28
|
-
payload = ResolveLinkPayload(
|
|
29
|
-
link=f"https://max.ru/{name}",
|
|
30
|
-
).model_dump(by_alias=True)
|
|
31
|
-
|
|
32
|
-
data = await self._send_and_wait(opcode=Opcode.LINK_INFO, payload=payload)
|
|
33
|
-
if data.get("payload", {}).get("error"):
|
|
34
|
-
MixinsUtils.handle_error(data)
|
|
35
|
-
|
|
36
|
-
channel = Channel.from_dict(data.get("payload", {}).get("chat", {}))
|
|
37
|
-
if channel not in self.channels:
|
|
38
|
-
self.channels.append(channel)
|
|
39
|
-
return channel
|
|
40
|
-
|
|
41
|
-
async def join_channel(self, link: str) -> Channel | None:
|
|
42
|
-
"""
|
|
43
|
-
Присоединяется к каналу по ссылке
|
|
44
|
-
|
|
45
|
-
:param link: Ссылка на канал
|
|
46
|
-
:type link: str
|
|
47
|
-
:return: Объект канала, если присоединение прошло успешно, иначе None
|
|
48
|
-
:rtype: Channel | None
|
|
49
|
-
"""
|
|
50
|
-
payload = JoinChatPayload(
|
|
51
|
-
link=link,
|
|
52
|
-
).model_dump(by_alias=True)
|
|
53
|
-
|
|
54
|
-
data = await self._send_and_wait(opcode=Opcode.CHAT_JOIN, payload=payload)
|
|
55
|
-
if data.get("payload", {}).get("error"):
|
|
56
|
-
MixinsUtils.handle_error(data)
|
|
57
|
-
|
|
58
|
-
channel = Channel.from_dict(data.get("payload", {}).get("chat", {}))
|
|
59
|
-
if channel not in self.channels:
|
|
60
|
-
self.channels.append(channel)
|
|
61
|
-
return channel
|
|
62
|
-
|
|
63
|
-
async def _query_members(
|
|
64
|
-
self, payload: GetGroupMembersPayload | SearchGroupMembersPayload
|
|
65
|
-
) -> tuple[list[Member], int | None]:
|
|
66
|
-
data = await self._send_and_wait(
|
|
67
|
-
opcode=Opcode.CHAT_MEMBERS,
|
|
68
|
-
payload=payload.model_dump(by_alias=True, exclude_none=True),
|
|
69
|
-
)
|
|
70
|
-
response_payload = data.get("payload", {})
|
|
71
|
-
if data.get("payload", {}).get("error"):
|
|
72
|
-
MixinsUtils.handle_error(data)
|
|
73
|
-
marker = response_payload.get("marker")
|
|
74
|
-
if isinstance(marker, str):
|
|
75
|
-
marker = int(marker)
|
|
76
|
-
elif isinstance(marker, int):
|
|
77
|
-
pass
|
|
78
|
-
elif marker is None:
|
|
79
|
-
# маркер может отсутствовать
|
|
80
|
-
pass
|
|
81
|
-
else:
|
|
82
|
-
raise ResponseStructureError("Invalid marker type in response")
|
|
83
|
-
members = response_payload.get("members")
|
|
84
|
-
member_list = []
|
|
85
|
-
if isinstance(members, list):
|
|
86
|
-
for item in members:
|
|
87
|
-
if not isinstance(item, dict):
|
|
88
|
-
raise ResponseStructureError("Invalid member structure in response")
|
|
89
|
-
member_list.append(Member.from_dict(item))
|
|
90
|
-
else:
|
|
91
|
-
raise ResponseStructureError("Invalid members type in response")
|
|
92
|
-
return member_list, marker
|
|
93
|
-
|
|
94
|
-
async def load_members(
|
|
95
|
-
self,
|
|
96
|
-
chat_id: int,
|
|
97
|
-
marker: int | None = DEFAULT_MARKER_VALUE,
|
|
98
|
-
count: int = DEFAULT_CHAT_MEMBERS_LIMIT,
|
|
99
|
-
) -> tuple[list[Member], int | None]:
|
|
100
|
-
"""
|
|
101
|
-
Загружает членов канала
|
|
102
|
-
|
|
103
|
-
:param chat_id: Идентификатор канала
|
|
104
|
-
:type chat_id: int
|
|
105
|
-
:param marker: Маркер для пагинации. По умолчанию DEFAULT_MARKER_VALUE
|
|
106
|
-
:type marker: int | None
|
|
107
|
-
:param count: Количество членов для загрузки. По умолчанию DEFAULT_CHAT_MEMBERS_LIMIT.
|
|
108
|
-
:type count: int
|
|
109
|
-
:return: Список участников канала и маркер для следующей страницы
|
|
110
|
-
:rtype: tuple[list[Member], int | None]
|
|
111
|
-
"""
|
|
112
|
-
|
|
113
|
-
payload = GetGroupMembersPayload(chat_id=chat_id, marker=marker, count=count)
|
|
114
|
-
return await self._query_members(payload)
|
|
115
|
-
|
|
116
|
-
async def find_members(self, chat_id: int, query: str) -> tuple[list[Member], int | None]:
|
|
117
|
-
"""
|
|
118
|
-
Поиск участников канала по строке
|
|
119
|
-
Внимание! веб-клиент всегда возвращает только определённое количество пользователей,
|
|
120
|
-
тоесть пагинация здесь не реализована!
|
|
121
|
-
|
|
122
|
-
:param chat_id: Идентификатор канала
|
|
123
|
-
:type chat_id: int
|
|
124
|
-
:param query: Строка для поиска участников
|
|
125
|
-
:type query: str
|
|
126
|
-
:return: Список участников канала
|
|
127
|
-
:rtype: tuple[list[Member], int | None]
|
|
128
|
-
"""
|
|
129
|
-
payload = SearchGroupMembersPayload(chat_id=chat_id, query=query)
|
|
130
|
-
return await self._query_members(payload)
|