maxapi-python 1.1.20__py3-none-any.whl → 1.2.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-1.1.20.dist-info → maxapi_python-1.2.1.dist-info}/METADATA +44 -54
- maxapi_python-1.2.1.dist-info/RECORD +32 -0
- pymax/core.py +79 -156
- pymax/crud.py +3 -7
- pymax/filters.py +158 -41
- pymax/formatter.py +1 -0
- pymax/formatting.py +4 -6
- pymax/interfaces.py +148 -8
- pymax/mixins/__init__.py +3 -0
- pymax/mixins/auth.py +229 -30
- pymax/mixins/channel.py +36 -37
- pymax/mixins/group.py +127 -8
- pymax/mixins/handler.py +163 -39
- pymax/mixins/message.py +251 -97
- pymax/mixins/scheduler.py +28 -0
- pymax/mixins/self.py +79 -40
- pymax/mixins/socket.py +254 -281
- pymax/mixins/user.py +63 -42
- pymax/mixins/websocket.py +145 -145
- pymax/payloads.py +12 -0
- pymax/static/constant.py +4 -2
- pymax/static/enum.py +5 -0
- maxapi_python-1.1.20.dist-info/RECORD +0 -31
- {maxapi_python-1.1.20.dist-info → maxapi_python-1.2.1.dist-info}/WHEEL +0 -0
- {maxapi_python-1.1.20.dist-info → maxapi_python-1.2.1.dist-info}/licenses/LICENSE +0 -0
pymax/mixins/auth.py
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import datetime
|
|
2
3
|
import re
|
|
3
4
|
import sys
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
7
|
+
import qrcode
|
|
8
|
+
|
|
6
9
|
from pymax.exceptions import Error
|
|
7
10
|
from pymax.interfaces import ClientProtocol
|
|
8
11
|
from pymax.mixins.utils import MixinsUtils
|
|
9
12
|
from pymax.payloads import RegisterPayload, RequestCodePayload, SendCodePayload
|
|
10
13
|
from pymax.static.constant import PHONE_REGEX
|
|
11
|
-
from pymax.static.enum import AuthType, Opcode
|
|
14
|
+
from pymax.static.enum import AuthType, DeviceType, Opcode
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
class AuthMixin(ClientProtocol):
|
|
@@ -19,15 +22,20 @@ class AuthMixin(ClientProtocol):
|
|
|
19
22
|
"""
|
|
20
23
|
Запрашивает код аутентификации для указанного номера телефона и возвращает временный токен.
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
Метод отправляет запрос на получение кода верификации на переданный номер телефона.
|
|
26
|
+
Используется в процессе аутентификации или регистрации.
|
|
24
27
|
|
|
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: Если сервер вернул ошибку.
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
.. note::
|
|
38
|
+
Используется только в пользовательском flow аутентификации.
|
|
31
39
|
"""
|
|
32
40
|
self.logger.info("Requesting auth code")
|
|
33
41
|
|
|
@@ -52,7 +60,54 @@ class AuthMixin(ClientProtocol):
|
|
|
52
60
|
self.logger.error("Invalid payload data received")
|
|
53
61
|
raise ValueError("Invalid payload data received")
|
|
54
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
|
+
|
|
55
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
|
+
"""
|
|
56
111
|
self.logger.info("Sending verification code")
|
|
57
112
|
|
|
58
113
|
payload = SendCodePayload(
|
|
@@ -78,32 +133,176 @@ class AuthMixin(ClientProtocol):
|
|
|
78
133
|
self.logger.error("Invalid payload data received")
|
|
79
134
|
raise ValueError("Invalid payload data received")
|
|
80
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
|
+
|
|
81
174
|
async def _login(self) -> None:
|
|
82
175
|
self.logger.info("Starting login flow")
|
|
83
176
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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")
|
|
88
181
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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", "")
|
|
94
198
|
|
|
95
|
-
login_resp = await self._send_code(code, temp_token)
|
|
96
|
-
token: str | None = (
|
|
97
|
-
login_resp.get("tokenAttrs", {}).get("LOGIN", {}).get("token")
|
|
98
|
-
)
|
|
99
199
|
if not token:
|
|
100
200
|
self.logger.critical("Failed to login, token not received")
|
|
101
201
|
raise ValueError("Failed to login, token not received")
|
|
102
202
|
|
|
103
203
|
self._token = token
|
|
104
|
-
self._database.update_auth_token(self._device_id, self._token)
|
|
204
|
+
self._database.update_auth_token((self._device_id), self._token)
|
|
105
205
|
self.logger.info("Login successful, token saved to database")
|
|
106
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
|
+
|
|
107
306
|
async def _submit_reg_info(
|
|
108
307
|
self, first_name: str, last_name: str | None, token: str
|
|
109
308
|
) -> dict[str, Any]:
|
|
@@ -116,9 +315,7 @@ class AuthMixin(ClientProtocol):
|
|
|
116
315
|
token=token,
|
|
117
316
|
).model_dump(by_alias=True)
|
|
118
317
|
|
|
119
|
-
data = await self._send_and_wait(
|
|
120
|
-
opcode=Opcode.AUTH_CONFIRM, payload=payload
|
|
121
|
-
)
|
|
318
|
+
data = await self._send_and_wait(opcode=Opcode.AUTH_CONFIRM, payload=payload)
|
|
122
319
|
if data.get("payload", {}).get("error"):
|
|
123
320
|
MixinsUtils.handle_error(data)
|
|
124
321
|
|
|
@@ -152,9 +349,7 @@ class AuthMixin(ClientProtocol):
|
|
|
152
349
|
raise ValueError("Invalid code format")
|
|
153
350
|
|
|
154
351
|
registration_response = await self._send_code(code, temp_token)
|
|
155
|
-
token
|
|
156
|
-
registration_response.get("tokenAttrs", {}).get("REGISTER", {}).get("token")
|
|
157
|
-
)
|
|
352
|
+
token = registration_response.get("tokenAttrs", {}).get("REGISTER", {}).get("token", "")
|
|
158
353
|
if not token:
|
|
159
354
|
self.logger.critical("Failed to register, token not received")
|
|
160
355
|
raise ValueError("Failed to register, token not received")
|
|
@@ -165,5 +360,9 @@ class AuthMixin(ClientProtocol):
|
|
|
165
360
|
self.logger.critical("Failed to register, token not received")
|
|
166
361
|
raise ValueError("Failed to register, token not received")
|
|
167
362
|
|
|
168
|
-
self.
|
|
169
|
-
self.logger.info("
|
|
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
CHANGED
|
@@ -12,23 +12,18 @@ from pymax.static.constant import (
|
|
|
12
12
|
DEFAULT_MARKER_VALUE,
|
|
13
13
|
)
|
|
14
14
|
from pymax.static.enum import Opcode
|
|
15
|
-
from pymax.types import Member
|
|
15
|
+
from pymax.types import Channel, Member
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class ChannelMixin(ClientProtocol):
|
|
19
|
-
async def resolve_channel_by_name(self, name: str) ->
|
|
19
|
+
async def resolve_channel_by_name(self, name: str) -> Channel | None:
|
|
20
20
|
"""
|
|
21
|
-
|
|
21
|
+
Получает информацию о канале по его имени
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
ResponseError: Ошибка в ответе сервера
|
|
28
|
-
ResponseStructureError: Ошибка структуры ответа сервера
|
|
29
|
-
|
|
30
|
-
Returns:
|
|
31
|
-
bool: True, если канал найден
|
|
23
|
+
:param name: Имя канала
|
|
24
|
+
:type name: str
|
|
25
|
+
:return: Объект Channel или None, если канал не найден
|
|
26
|
+
:rtype: Channel | None
|
|
32
27
|
"""
|
|
33
28
|
payload = ResolveLinkPayload(
|
|
34
29
|
link=f"https://max.ru/{name}",
|
|
@@ -37,21 +32,20 @@ class ChannelMixin(ClientProtocol):
|
|
|
37
32
|
data = await self._send_and_wait(opcode=Opcode.LINK_INFO, payload=payload)
|
|
38
33
|
if data.get("payload", {}).get("error"):
|
|
39
34
|
MixinsUtils.handle_error(data)
|
|
40
|
-
return True
|
|
41
35
|
|
|
42
|
-
|
|
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:
|
|
43
42
|
"""
|
|
44
43
|
Присоединяется к каналу по ссылке
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
ResponseError: Ошибка в ответе сервера
|
|
51
|
-
ResponseStructureError: Ошибка структуры ответа сервера
|
|
52
|
-
|
|
53
|
-
Returns:
|
|
54
|
-
bool: True, если присоединение прошло успешно
|
|
45
|
+
:param link: Ссылка на канал
|
|
46
|
+
:type link: str
|
|
47
|
+
:return: Объект канала, если присоединение прошло успешно, иначе None
|
|
48
|
+
:rtype: Channel | None
|
|
55
49
|
"""
|
|
56
50
|
payload = JoinChatPayload(
|
|
57
51
|
link=link,
|
|
@@ -60,7 +54,11 @@ class ChannelMixin(ClientProtocol):
|
|
|
60
54
|
data = await self._send_and_wait(opcode=Opcode.CHAT_JOIN, payload=payload)
|
|
61
55
|
if data.get("payload", {}).get("error"):
|
|
62
56
|
MixinsUtils.handle_error(data)
|
|
63
|
-
|
|
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
|
|
64
62
|
|
|
65
63
|
async def _query_members(
|
|
66
64
|
self, payload: GetGroupMembersPayload | SearchGroupMembersPayload
|
|
@@ -102,13 +100,14 @@ class ChannelMixin(ClientProtocol):
|
|
|
102
100
|
"""
|
|
103
101
|
Загружает членов канала
|
|
104
102
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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]
|
|
112
111
|
"""
|
|
113
112
|
|
|
114
113
|
payload = GetGroupMembersPayload(chat_id=chat_id, marker=marker, count=count)
|
|
@@ -122,12 +121,12 @@ class ChannelMixin(ClientProtocol):
|
|
|
122
121
|
Внимание! веб-клиент всегда возвращает только определённое количество пользователей,
|
|
123
122
|
тоесть пагинация здесь не реализована!
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
124
|
+
:param chat_id: Идентификатор канала
|
|
125
|
+
:type chat_id: int
|
|
126
|
+
:param query: Строка для поиска участников
|
|
127
|
+
:type query: str
|
|
128
|
+
:return: Список участников канала
|
|
129
|
+
:rtype: tuple[list[Member], int | None]
|
|
131
130
|
"""
|
|
132
131
|
payload = SearchGroupMembersPayload(chat_id=chat_id, query=query)
|
|
133
132
|
return await self._query_members(payload)
|
pymax/mixins/group.py
CHANGED
|
@@ -10,9 +10,11 @@ from pymax.payloads import (
|
|
|
10
10
|
CreateGroupAttach,
|
|
11
11
|
CreateGroupMessage,
|
|
12
12
|
CreateGroupPayload,
|
|
13
|
+
FetchChatsPayload,
|
|
13
14
|
GetChatInfoPayload,
|
|
14
15
|
InviteUsersPayload,
|
|
15
16
|
JoinChatPayload,
|
|
17
|
+
LeaveChatPayload,
|
|
16
18
|
RemoveUsersPayload,
|
|
17
19
|
ReworkInviteLinkPayload,
|
|
18
20
|
)
|
|
@@ -74,7 +76,7 @@ class GroupMixin(ClientProtocol):
|
|
|
74
76
|
chat_id: int,
|
|
75
77
|
user_ids: list[int],
|
|
76
78
|
show_history: bool = True,
|
|
77
|
-
) ->
|
|
79
|
+
) -> Chat | None:
|
|
78
80
|
"""
|
|
79
81
|
Приглашает пользователей в группу
|
|
80
82
|
|
|
@@ -84,7 +86,7 @@ class GroupMixin(ClientProtocol):
|
|
|
84
86
|
show_history (bool, optional): Флаг оповещения. Defaults to True.
|
|
85
87
|
|
|
86
88
|
Returns:
|
|
87
|
-
|
|
89
|
+
Chat | None: Объект Chat или None при ошибке.
|
|
88
90
|
"""
|
|
89
91
|
payload = InviteUsersPayload(
|
|
90
92
|
chat_id=chat_id,
|
|
@@ -109,7 +111,26 @@ class GroupMixin(ClientProtocol):
|
|
|
109
111
|
idx = self.chats.index(cached_chat)
|
|
110
112
|
self.chats[idx] = chat
|
|
111
113
|
|
|
112
|
-
return
|
|
114
|
+
return chat
|
|
115
|
+
|
|
116
|
+
async def invite_users_to_channel(
|
|
117
|
+
self,
|
|
118
|
+
chat_id: int,
|
|
119
|
+
user_ids: list[int],
|
|
120
|
+
show_history: bool = True,
|
|
121
|
+
) -> Chat | None:
|
|
122
|
+
"""
|
|
123
|
+
Приглашает пользователей в канал
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
chat_id (int): ID канала.
|
|
127
|
+
user_ids (list[int]): Список идентификаторов пользователей.
|
|
128
|
+
show_history (bool, optional): Флаг оповещения. Defaults to True.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Chat | None: Объект Chat или None при ошибке.
|
|
132
|
+
"""
|
|
133
|
+
return await self.invite_users_to_group(chat_id, user_ids, show_history)
|
|
113
134
|
|
|
114
135
|
async def remove_users_from_group(
|
|
115
136
|
self,
|
|
@@ -117,6 +138,17 @@ class GroupMixin(ClientProtocol):
|
|
|
117
138
|
user_ids: list[int],
|
|
118
139
|
clean_msg_period: int,
|
|
119
140
|
) -> bool:
|
|
141
|
+
"""
|
|
142
|
+
Удаляет пользователей из группы
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
chat_id (int): ID группы.
|
|
146
|
+
user_ids (list[int]): Список идентификаторов пользователей.
|
|
147
|
+
clean_msg_period (int): Период очистки сообщений.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
bool: True, если удаление прошло успешно, иначе False.
|
|
151
|
+
"""
|
|
120
152
|
payload = RemoveUsersPayload(
|
|
121
153
|
chat_id=chat_id,
|
|
122
154
|
user_ids=user_ids,
|
|
@@ -150,6 +182,19 @@ class GroupMixin(ClientProtocol):
|
|
|
150
182
|
only_admin_can_call: bool | None = None,
|
|
151
183
|
members_can_see_private_link: bool | None = None,
|
|
152
184
|
) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Изменяет настройки группы
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
chat_id (int): ID группы.
|
|
190
|
+
all_can_pin_message (bool | None, optional): Все могут закреплять сообщения. Defaults to None.
|
|
191
|
+
only_owner_can_change_icon_title (bool | None, optional): Только владелец может менять иконку и название. Defaults to None.
|
|
192
|
+
only_admin_can_add_member (bool | None, optional): Только администраторы могут добавлять участников. Defaults to None.
|
|
193
|
+
only_admin_can_call (bool | None, optional): Только администраторы могут звонить. Defaults to None.
|
|
194
|
+
members_can_see_private_link (bool | None, optional): Участники могут видеть приватную ссылку. Defaults to None.
|
|
195
|
+
Returns:
|
|
196
|
+
None
|
|
197
|
+
"""
|
|
153
198
|
payload = ChangeGroupSettingsPayload(
|
|
154
199
|
chat_id=chat_id,
|
|
155
200
|
options=ChangeGroupSettingsOptions(
|
|
@@ -181,6 +226,17 @@ class GroupMixin(ClientProtocol):
|
|
|
181
226
|
name: str | None,
|
|
182
227
|
description: str | None = None,
|
|
183
228
|
) -> None:
|
|
229
|
+
"""
|
|
230
|
+
Изменяет профиль группы
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
chat_id (int): ID группы.
|
|
234
|
+
name (str | None): Название группы.
|
|
235
|
+
description (str | None, optional): Описание группы. Defaults to None.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
None
|
|
239
|
+
"""
|
|
184
240
|
payload = ChangeGroupProfilePayload(
|
|
185
241
|
chat_id=chat_id,
|
|
186
242
|
theme=name,
|
|
@@ -264,11 +320,10 @@ class GroupMixin(ClientProtocol):
|
|
|
264
320
|
"""
|
|
265
321
|
Получает информацию о группах по их ID
|
|
266
322
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
list[Chat]: Список объектов Chat.
|
|
323
|
+
:param chat_ids: Список идентификаторов групп.
|
|
324
|
+
:type chat_ids: list[int]
|
|
325
|
+
:return: Список объектов Chat.
|
|
326
|
+
:rtype: list[Chat]
|
|
272
327
|
"""
|
|
273
328
|
missed_chat_ids = [
|
|
274
329
|
chat_id for chat_id in chat_ids if await self._get_chat(chat_id) is None
|
|
@@ -318,3 +373,67 @@ class GroupMixin(ClientProtocol):
|
|
|
318
373
|
if not chats:
|
|
319
374
|
raise Error("no_chat", "Chat not found in response", "Chat Error")
|
|
320
375
|
return chats[0]
|
|
376
|
+
|
|
377
|
+
async def leave_group(self, chat_id: int) -> None:
|
|
378
|
+
"""
|
|
379
|
+
Покидает группу
|
|
380
|
+
|
|
381
|
+
:param chat_id: Идентификатор группы.
|
|
382
|
+
:type chat_id: int
|
|
383
|
+
:return: None
|
|
384
|
+
:rtype: None
|
|
385
|
+
"""
|
|
386
|
+
payload = LeaveChatPayload(chat_id=chat_id).model_dump(by_alias=True)
|
|
387
|
+
|
|
388
|
+
data = await self._send_and_wait(opcode=Opcode.CHAT_LEAVE, payload=payload)
|
|
389
|
+
|
|
390
|
+
if data.get("payload", {}).get("error"):
|
|
391
|
+
MixinsUtils.handle_error(data)
|
|
392
|
+
|
|
393
|
+
cached_chat = await self._get_chat(chat_id)
|
|
394
|
+
if cached_chat is not None:
|
|
395
|
+
self.chats.remove(cached_chat)
|
|
396
|
+
|
|
397
|
+
async def leave_channel(self, chat_id: int) -> None:
|
|
398
|
+
"""
|
|
399
|
+
Покидает канал
|
|
400
|
+
|
|
401
|
+
:param chat_id: Идентификатор канала.
|
|
402
|
+
:type chat_id: int
|
|
403
|
+
:return: None
|
|
404
|
+
:rtype: None
|
|
405
|
+
"""
|
|
406
|
+
await self.leave_group(chat_id)
|
|
407
|
+
|
|
408
|
+
async def fetch_chats(self, marker: int | None = None) -> list[Chat]:
|
|
409
|
+
"""
|
|
410
|
+
Загружает список чатов
|
|
411
|
+
|
|
412
|
+
:param marker: Маркер для пагинации, по умолчанию None
|
|
413
|
+
:type marker: int | None
|
|
414
|
+
:return: Список объектов Chat
|
|
415
|
+
:rtype: list[Chat]
|
|
416
|
+
"""
|
|
417
|
+
if marker is None:
|
|
418
|
+
marker = int(time.time() * 1000)
|
|
419
|
+
|
|
420
|
+
payload = FetchChatsPayload(marker=marker).model_dump(by_alias=True)
|
|
421
|
+
|
|
422
|
+
data = await self._send_and_wait(opcode=Opcode.CHATS_LIST, payload=payload)
|
|
423
|
+
|
|
424
|
+
if data.get("payload", {}).get("error"):
|
|
425
|
+
MixinsUtils.handle_error(data)
|
|
426
|
+
|
|
427
|
+
chats_data = data["payload"].get("chats", [])
|
|
428
|
+
chats: list[Chat] = []
|
|
429
|
+
for chat_dict in chats_data:
|
|
430
|
+
chat = Chat.from_dict(chat_dict)
|
|
431
|
+
chats.append(chat)
|
|
432
|
+
cached_chat = await self._get_chat(chat.id)
|
|
433
|
+
if cached_chat is None:
|
|
434
|
+
self.chats.append(chat)
|
|
435
|
+
else:
|
|
436
|
+
idx = self.chats.index(cached_chat)
|
|
437
|
+
self.chats[idx] = chat
|
|
438
|
+
|
|
439
|
+
return chats
|