maxapi-python 1.2.2__py3-none-any.whl → 1.2.4__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.2.2.dist-info → maxapi_python-1.2.4.dist-info}/METADATA +12 -7
- maxapi_python-1.2.4.dist-info/RECORD +33 -0
- pymax/core.py +55 -37
- pymax/files.py +33 -7
- pymax/interfaces.py +410 -114
- pymax/mixins/auth.py +2 -2
- pymax/mixins/channel.py +3 -5
- pymax/mixins/group.py +33 -14
- pymax/mixins/handler.py +4 -10
- pymax/mixins/message.py +64 -88
- pymax/mixins/scheduler.py +1 -1
- pymax/mixins/self.py +76 -11
- pymax/mixins/socket.py +4 -327
- pymax/mixins/telemetry.py +2 -4
- pymax/mixins/user.py +3 -5
- pymax/mixins/websocket.py +5 -354
- pymax/payloads.py +11 -1
- pymax/protocols.py +123 -0
- pymax/static/constant.py +69 -8
- pymax/static/enum.py +6 -0
- pymax/types.py +82 -28
- pymax/utils.py +90 -0
- maxapi_python-1.2.2.dist-info/RECORD +0 -32
- pymax/mixins/utils.py +0 -27
- {maxapi_python-1.2.2.dist-info → maxapi_python-1.2.4.dist-info}/WHEEL +0 -0
- {maxapi_python-1.2.2.dist-info → maxapi_python-1.2.4.dist-info}/licenses/LICENSE +0 -0
pymax/mixins/websocket.py
CHANGED
|
@@ -1,36 +1,25 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
|
-
import time
|
|
4
|
-
from collections.abc import Callable
|
|
5
3
|
from typing import Any
|
|
6
4
|
|
|
7
5
|
import websockets
|
|
8
6
|
from typing_extensions import override
|
|
9
7
|
|
|
10
|
-
from pymax.exceptions import
|
|
11
|
-
from pymax.
|
|
12
|
-
from pymax.
|
|
13
|
-
from pymax.mixins.utils import MixinsUtils
|
|
14
|
-
from pymax.payloads import BaseWebSocketMessage, SyncPayload, UserAgentPayload
|
|
8
|
+
from pymax.exceptions import WebSocketNotConnectedError
|
|
9
|
+
from pymax.interfaces import BaseTransport
|
|
10
|
+
from pymax.payloads import UserAgentPayload
|
|
15
11
|
from pymax.static.constant import (
|
|
16
|
-
DEFAULT_PING_INTERVAL,
|
|
17
12
|
DEFAULT_TIMEOUT,
|
|
18
13
|
RECV_LOOP_BACKOFF_DELAY,
|
|
19
14
|
WEBSOCKET_ORIGIN,
|
|
20
15
|
)
|
|
21
|
-
from pymax.static.enum import
|
|
16
|
+
from pymax.static.enum import Opcode
|
|
22
17
|
from pymax.types import (
|
|
23
|
-
Channel,
|
|
24
18
|
Chat,
|
|
25
|
-
Dialog,
|
|
26
|
-
Me,
|
|
27
|
-
Message,
|
|
28
|
-
ReactionCounter,
|
|
29
|
-
ReactionInfo,
|
|
30
19
|
)
|
|
31
20
|
|
|
32
21
|
|
|
33
|
-
class WebSocketMixin(
|
|
22
|
+
class WebSocketMixin(BaseTransport):
|
|
34
23
|
@property
|
|
35
24
|
def ws(self) -> websockets.ClientConnection:
|
|
36
25
|
if self._ws is None or not self.is_connected:
|
|
@@ -38,34 +27,6 @@ class WebSocketMixin(ClientProtocol):
|
|
|
38
27
|
raise WebSocketNotConnectedError
|
|
39
28
|
return self._ws
|
|
40
29
|
|
|
41
|
-
def _make_message(
|
|
42
|
-
self, opcode: Opcode, payload: dict[str, Any], cmd: int = 0
|
|
43
|
-
) -> dict[str, Any]:
|
|
44
|
-
self._seq += 1
|
|
45
|
-
|
|
46
|
-
msg = BaseWebSocketMessage(
|
|
47
|
-
cmd=cmd,
|
|
48
|
-
seq=self._seq,
|
|
49
|
-
opcode=opcode.value,
|
|
50
|
-
payload=payload,
|
|
51
|
-
).model_dump(by_alias=True)
|
|
52
|
-
|
|
53
|
-
self.logger.debug("make_message opcode=%s cmd=%s seq=%s", opcode, cmd, self._seq)
|
|
54
|
-
return msg
|
|
55
|
-
|
|
56
|
-
async def _send_interactive_ping(self) -> None:
|
|
57
|
-
while self.is_connected:
|
|
58
|
-
try:
|
|
59
|
-
await self._send_and_wait(
|
|
60
|
-
opcode=Opcode.PING,
|
|
61
|
-
payload={"interactive": True},
|
|
62
|
-
cmd=0,
|
|
63
|
-
)
|
|
64
|
-
self.logger.debug("Interactive ping sent successfully")
|
|
65
|
-
except Exception:
|
|
66
|
-
self.logger.warning("Interactive ping failed", exc_info=True)
|
|
67
|
-
await asyncio.sleep(DEFAULT_PING_INTERVAL)
|
|
68
|
-
|
|
69
30
|
async def connect(self, user_agent: UserAgentPayload | None = None) -> dict[str, Any] | None:
|
|
70
31
|
"""
|
|
71
32
|
Устанавливает соединение WebSocket с сервером и выполняет handshake.
|
|
@@ -99,159 +60,6 @@ class WebSocketMixin(ClientProtocol):
|
|
|
99
60
|
self.logger.info("WebSocket connected, starting handshake")
|
|
100
61
|
return await self._handshake(user_agent)
|
|
101
62
|
|
|
102
|
-
async def _handshake(self, user_agent: UserAgentPayload) -> dict[str, Any]:
|
|
103
|
-
self.logger.debug(
|
|
104
|
-
"Sending handshake with user_agent keys=%s",
|
|
105
|
-
user_agent.model_dump(by_alias=True).keys(),
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
user_agent_json = user_agent.model_dump(by_alias=True)
|
|
109
|
-
resp = await self._send_and_wait(
|
|
110
|
-
opcode=Opcode.SESSION_INIT,
|
|
111
|
-
payload={"deviceId": str(self._device_id), "userAgent": user_agent_json},
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
if resp.get("payload", {}).get("error"):
|
|
115
|
-
MixinsUtils.handle_error(resp)
|
|
116
|
-
|
|
117
|
-
self.logger.info("Handshake completed")
|
|
118
|
-
return resp
|
|
119
|
-
|
|
120
|
-
async def _process_message_handler(
|
|
121
|
-
self,
|
|
122
|
-
handler: Callable[[Message], Any],
|
|
123
|
-
filter: BaseFilter[Message] | None,
|
|
124
|
-
message: Message,
|
|
125
|
-
):
|
|
126
|
-
result = None
|
|
127
|
-
if filter:
|
|
128
|
-
if filter(message):
|
|
129
|
-
result = handler(message)
|
|
130
|
-
else:
|
|
131
|
-
return
|
|
132
|
-
else:
|
|
133
|
-
result = handler(message)
|
|
134
|
-
if asyncio.iscoroutine(result):
|
|
135
|
-
self._create_safe_task(result, name=f"handler-{handler.__name__}")
|
|
136
|
-
|
|
137
|
-
def _parse_json(self, raw: Any) -> dict[str, Any] | None:
|
|
138
|
-
try:
|
|
139
|
-
return json.loads(raw)
|
|
140
|
-
except Exception:
|
|
141
|
-
self.logger.warning("JSON parse error", exc_info=True)
|
|
142
|
-
return None
|
|
143
|
-
|
|
144
|
-
def _handle_pending(self, seq: int | None, data: dict) -> bool:
|
|
145
|
-
if isinstance(seq, int):
|
|
146
|
-
fut = self._pending.get(seq)
|
|
147
|
-
if fut and not fut.done():
|
|
148
|
-
fut.set_result(data)
|
|
149
|
-
self.logger.debug("Matched response for pending seq=%s", seq)
|
|
150
|
-
return True
|
|
151
|
-
return False
|
|
152
|
-
|
|
153
|
-
async def _handle_incoming_queue(self, data: dict[str, Any]) -> None:
|
|
154
|
-
if self._incoming:
|
|
155
|
-
try:
|
|
156
|
-
self._incoming.put_nowait(data)
|
|
157
|
-
except asyncio.QueueFull:
|
|
158
|
-
self.logger.warning(
|
|
159
|
-
"Incoming queue full; dropping message seq=%s", data.get("seq")
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
async def _handle_file_upload(self, data: dict[str, Any]) -> None:
|
|
163
|
-
if data.get("opcode") != Opcode.NOTIF_ATTACH:
|
|
164
|
-
return
|
|
165
|
-
payload = data.get("payload", {})
|
|
166
|
-
for key in ("fileId", "videoId"):
|
|
167
|
-
id_ = payload.get(key)
|
|
168
|
-
if id_ is not None:
|
|
169
|
-
fut = self._file_upload_waiters.pop(id_, None)
|
|
170
|
-
if fut and not fut.done():
|
|
171
|
-
fut.set_result(data)
|
|
172
|
-
self.logger.debug("Fulfilled file upload waiter for %s=%s", key, id_)
|
|
173
|
-
|
|
174
|
-
async def _handle_message_notifications(self, data: dict) -> None:
|
|
175
|
-
if data.get("opcode") != Opcode.NOTIF_MESSAGE.value:
|
|
176
|
-
return
|
|
177
|
-
payload = data.get("payload", {})
|
|
178
|
-
msg = Message.from_dict(payload)
|
|
179
|
-
if not msg:
|
|
180
|
-
return
|
|
181
|
-
handlers_map = {
|
|
182
|
-
MessageStatus.EDITED: self._on_message_edit_handlers,
|
|
183
|
-
MessageStatus.REMOVED: self._on_message_delete_handlers,
|
|
184
|
-
}
|
|
185
|
-
if msg.status and msg.status in handlers_map:
|
|
186
|
-
for handler, filter in handlers_map[msg.status]:
|
|
187
|
-
await self._process_message_handler(handler, filter, msg)
|
|
188
|
-
if msg.status is None:
|
|
189
|
-
for handler, filter in self._on_message_handlers:
|
|
190
|
-
await self._process_message_handler(handler, filter, msg)
|
|
191
|
-
|
|
192
|
-
async def _handle_reactions(self, data: dict):
|
|
193
|
-
if data.get("opcode") != Opcode.NOTIF_MSG_REACTIONS_CHANGED:
|
|
194
|
-
return
|
|
195
|
-
|
|
196
|
-
payload = data.get("payload", {})
|
|
197
|
-
chat_id = payload.get("chatId")
|
|
198
|
-
message_id = payload.get("messageId")
|
|
199
|
-
|
|
200
|
-
if not (chat_id and message_id):
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
total_count = payload.get("totalCount")
|
|
204
|
-
your_reaction = payload.get("yourReaction")
|
|
205
|
-
counters = [ReactionCounter.from_dict(c) for c in payload.get("counters", [])]
|
|
206
|
-
|
|
207
|
-
reaction_info = ReactionInfo(
|
|
208
|
-
total_count=total_count,
|
|
209
|
-
your_reaction=your_reaction,
|
|
210
|
-
counters=counters,
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
for handler in self._on_reaction_change_handlers:
|
|
214
|
-
try:
|
|
215
|
-
result = handler(message_id, chat_id, reaction_info)
|
|
216
|
-
if asyncio.iscoroutine(result):
|
|
217
|
-
await result
|
|
218
|
-
except Exception as e:
|
|
219
|
-
self.logger.exception("Error in on_reaction_change_handler: %s", e)
|
|
220
|
-
|
|
221
|
-
async def _handle_chat_updates(self, data: dict) -> None:
|
|
222
|
-
if data.get("opcode") != Opcode.NOTIF_CHAT:
|
|
223
|
-
return
|
|
224
|
-
|
|
225
|
-
payload = data.get("payload", {})
|
|
226
|
-
chat_data = payload.get("chat", {})
|
|
227
|
-
chat = Chat.from_dict(chat_data)
|
|
228
|
-
if not chat:
|
|
229
|
-
return
|
|
230
|
-
|
|
231
|
-
for handler in self._on_chat_update_handlers:
|
|
232
|
-
try:
|
|
233
|
-
result = handler(chat)
|
|
234
|
-
if asyncio.iscoroutine(result):
|
|
235
|
-
await result
|
|
236
|
-
except Exception as e:
|
|
237
|
-
self.logger.exception("Error in on_chat_update_handler: %s", e)
|
|
238
|
-
|
|
239
|
-
async def _handle_raw_receive(self, data: dict[str, Any]) -> None:
|
|
240
|
-
for handler in self._on_raw_receive_handlers:
|
|
241
|
-
try:
|
|
242
|
-
result = handler(data)
|
|
243
|
-
if asyncio.iscoroutine(result):
|
|
244
|
-
await result
|
|
245
|
-
except Exception as e:
|
|
246
|
-
self.logger.exception("Error in on_raw_receive_handler: %s", e)
|
|
247
|
-
|
|
248
|
-
async def _dispatch_incoming(self, data: dict[str, Any]) -> None:
|
|
249
|
-
await self._handle_raw_receive(data)
|
|
250
|
-
await self._handle_file_upload(data)
|
|
251
|
-
await self._handle_message_notifications(data)
|
|
252
|
-
await self._handle_reactions(data)
|
|
253
|
-
await self._handle_chat_updates(data)
|
|
254
|
-
|
|
255
63
|
async def _recv_loop(self) -> None:
|
|
256
64
|
if self._ws is None:
|
|
257
65
|
self.logger.warning("Recv loop started without websocket instance")
|
|
@@ -291,38 +99,6 @@ class WebSocketMixin(ClientProtocol):
|
|
|
291
99
|
self.logger.exception("Error in recv_loop; backing off briefly")
|
|
292
100
|
await asyncio.sleep(RECV_LOOP_BACKOFF_DELAY)
|
|
293
101
|
|
|
294
|
-
def _log_task_exception(self, fut: asyncio.Future[Any]) -> None:
|
|
295
|
-
try:
|
|
296
|
-
fut.result()
|
|
297
|
-
except asyncio.CancelledError:
|
|
298
|
-
pass
|
|
299
|
-
except Exception as e:
|
|
300
|
-
self.logger.exception("Error retrieving task exception: %s", e)
|
|
301
|
-
|
|
302
|
-
async def _queue_message(
|
|
303
|
-
self,
|
|
304
|
-
opcode: int,
|
|
305
|
-
payload: dict[str, Any],
|
|
306
|
-
cmd: int = 0,
|
|
307
|
-
timeout: float = DEFAULT_TIMEOUT,
|
|
308
|
-
max_retries: int = 3,
|
|
309
|
-
) -> None:
|
|
310
|
-
if self._outgoing is None:
|
|
311
|
-
self.logger.warning("Outgoing queue not initialized")
|
|
312
|
-
return
|
|
313
|
-
|
|
314
|
-
message = {
|
|
315
|
-
"opcode": opcode,
|
|
316
|
-
"payload": payload,
|
|
317
|
-
"cmd": cmd,
|
|
318
|
-
"timeout": timeout,
|
|
319
|
-
"retry_count": 0,
|
|
320
|
-
"max_retries": max_retries,
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
await self._outgoing.put(message)
|
|
324
|
-
self.logger.debug("Message queued for sending")
|
|
325
|
-
|
|
326
102
|
@override
|
|
327
103
|
async def _send_and_wait(
|
|
328
104
|
self,
|
|
@@ -358,131 +134,6 @@ class WebSocketMixin(ClientProtocol):
|
|
|
358
134
|
finally:
|
|
359
135
|
self._pending.pop(msg["seq"], None)
|
|
360
136
|
|
|
361
|
-
async def _outgoing_loop(self) -> None:
|
|
362
|
-
while self.is_connected:
|
|
363
|
-
try:
|
|
364
|
-
if self._outgoing is None:
|
|
365
|
-
await asyncio.sleep(0.1)
|
|
366
|
-
continue
|
|
367
|
-
|
|
368
|
-
if self._circuit_breaker:
|
|
369
|
-
if time.time() - self._last_error_time > 60:
|
|
370
|
-
self._circuit_breaker = False
|
|
371
|
-
self._error_count = 0
|
|
372
|
-
self.logger.info("Circuit breaker reset")
|
|
373
|
-
else:
|
|
374
|
-
await asyncio.sleep(5)
|
|
375
|
-
continue
|
|
376
|
-
|
|
377
|
-
message = await self._outgoing.get() # TODO: persistent msg q mb?
|
|
378
|
-
if not message:
|
|
379
|
-
continue
|
|
380
|
-
|
|
381
|
-
retry_count = message.get("retry_count", 0)
|
|
382
|
-
max_retries = message.get("max_retries", 3)
|
|
383
|
-
|
|
384
|
-
try:
|
|
385
|
-
await self._send_and_wait(
|
|
386
|
-
opcode=message["opcode"],
|
|
387
|
-
payload=message["payload"],
|
|
388
|
-
cmd=message.get("cmd", 0),
|
|
389
|
-
timeout=message.get("timeout", DEFAULT_TIMEOUT),
|
|
390
|
-
)
|
|
391
|
-
self.logger.debug("Message sent successfully from queue")
|
|
392
|
-
self._error_count = max(0, self._error_count - 1)
|
|
393
|
-
except Exception as e:
|
|
394
|
-
self._error_count += 1
|
|
395
|
-
self._last_error_time = time.time()
|
|
396
|
-
|
|
397
|
-
if self._error_count > 10:
|
|
398
|
-
self._circuit_breaker = True
|
|
399
|
-
self.logger.warning(
|
|
400
|
-
"Circuit breaker activated due to %d consecutive errors",
|
|
401
|
-
self._error_count,
|
|
402
|
-
)
|
|
403
|
-
await self._outgoing.put(message)
|
|
404
|
-
continue
|
|
405
|
-
|
|
406
|
-
retry_delay = self._get_retry_delay(e, retry_count)
|
|
407
|
-
self.logger.warning(
|
|
408
|
-
"Failed to send message from queue: %s (delay: %ds)",
|
|
409
|
-
e,
|
|
410
|
-
retry_delay,
|
|
411
|
-
)
|
|
412
|
-
|
|
413
|
-
if retry_count < max_retries:
|
|
414
|
-
message["retry_count"] = retry_count + 1
|
|
415
|
-
await asyncio.sleep(retry_delay)
|
|
416
|
-
await self._outgoing.put(message)
|
|
417
|
-
else:
|
|
418
|
-
self.logger.error(
|
|
419
|
-
"Message failed after %d retries, dropping",
|
|
420
|
-
max_retries,
|
|
421
|
-
)
|
|
422
|
-
|
|
423
|
-
except Exception:
|
|
424
|
-
self.logger.exception("Error in outgoing loop")
|
|
425
|
-
await asyncio.sleep(1)
|
|
426
|
-
|
|
427
|
-
def _get_retry_delay(self, error: Exception, retry_count: int) -> float:
|
|
428
|
-
if isinstance(error, (ConnectionError, OSError)):
|
|
429
|
-
return 1.0
|
|
430
|
-
elif isinstance(error, TimeoutError):
|
|
431
|
-
return 5.0
|
|
432
|
-
elif isinstance(error, WebSocketNotConnectedError):
|
|
433
|
-
return 2.0
|
|
434
|
-
else:
|
|
435
|
-
return float(2**retry_count)
|
|
436
|
-
|
|
437
|
-
async def _sync(self, user_agent: UserAgentPayload) -> None:
|
|
438
|
-
self.logger.info("Starting initial sync")
|
|
439
|
-
|
|
440
|
-
payload = SyncPayload(
|
|
441
|
-
interactive=True,
|
|
442
|
-
token=self._token,
|
|
443
|
-
chats_sync=0,
|
|
444
|
-
contacts_sync=0,
|
|
445
|
-
presence_sync=0,
|
|
446
|
-
drafts_sync=0,
|
|
447
|
-
chats_count=40,
|
|
448
|
-
user_agent=user_agent,
|
|
449
|
-
).model_dump(by_alias=True)
|
|
450
|
-
try:
|
|
451
|
-
data = await self._send_and_wait(opcode=Opcode.LOGIN, payload=payload)
|
|
452
|
-
raw_payload = data.get("payload", {})
|
|
453
|
-
|
|
454
|
-
if error := raw_payload.get("error"):
|
|
455
|
-
MixinsUtils.handle_error(data)
|
|
456
|
-
|
|
457
|
-
for raw_chat in raw_payload.get("chats", []):
|
|
458
|
-
try:
|
|
459
|
-
if raw_chat.get("type") == ChatType.DIALOG.value:
|
|
460
|
-
self.dialogs.append(Dialog.from_dict(raw_chat))
|
|
461
|
-
elif raw_chat.get("type") == ChatType.CHAT.value:
|
|
462
|
-
self.chats.append(Chat.from_dict(raw_chat))
|
|
463
|
-
elif raw_chat.get("type") == ChatType.CHANNEL.value:
|
|
464
|
-
self.channels.append(Channel.from_dict(raw_chat))
|
|
465
|
-
except Exception:
|
|
466
|
-
self.logger.exception("Error parsing chat entry")
|
|
467
|
-
|
|
468
|
-
if raw_payload.get("profile", {}).get("contact"):
|
|
469
|
-
self.me = Me.from_dict(raw_payload.get("profile", {}).get("contact", {}))
|
|
470
|
-
|
|
471
|
-
self.logger.info(
|
|
472
|
-
"Sync completed: dialogs=%d chats=%d channels=%d",
|
|
473
|
-
len(self.dialogs),
|
|
474
|
-
len(self.chats),
|
|
475
|
-
len(self.channels),
|
|
476
|
-
)
|
|
477
|
-
|
|
478
|
-
except Exception as e:
|
|
479
|
-
self.logger.exception("Sync failed")
|
|
480
|
-
self.is_connected = False
|
|
481
|
-
if self._ws:
|
|
482
|
-
await self._ws.close()
|
|
483
|
-
self._ws = None
|
|
484
|
-
raise e
|
|
485
|
-
|
|
486
137
|
@override
|
|
487
138
|
async def _get_chat(self, chat_id: int) -> Chat | None:
|
|
488
139
|
for chat in self.chats:
|
pymax/payloads.py
CHANGED
|
@@ -15,7 +15,7 @@ from pymax.static.constant import (
|
|
|
15
15
|
DEFAULT_TIMEZONE,
|
|
16
16
|
DEFAULT_USER_AGENT,
|
|
17
17
|
)
|
|
18
|
-
from pymax.static.enum import AttachType, AuthType, ContactAction
|
|
18
|
+
from pymax.static.enum import AttachType, AuthType, ContactAction, ReadAction
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def to_camel(string: str) -> str:
|
|
@@ -97,6 +97,7 @@ class ReplyLink(CamelModel):
|
|
|
97
97
|
|
|
98
98
|
class UploadPayload(CamelModel):
|
|
99
99
|
count: int = 1
|
|
100
|
+
profile: bool = False
|
|
100
101
|
|
|
101
102
|
|
|
102
103
|
class AttachPhotoPayload(CamelModel):
|
|
@@ -168,6 +169,8 @@ class ChangeProfilePayload(CamelModel):
|
|
|
168
169
|
first_name: str
|
|
169
170
|
last_name: str | None = None
|
|
170
171
|
description: str | None = None
|
|
172
|
+
photo_token: str | None = None
|
|
173
|
+
avatar_type: str = "USER_AVATAR" # TODO: вынести гада в энам
|
|
171
174
|
|
|
172
175
|
|
|
173
176
|
class ResolveLinkPayload(CamelModel):
|
|
@@ -355,3 +358,10 @@ class LeaveChatPayload(CamelModel):
|
|
|
355
358
|
|
|
356
359
|
class FetchChatsPayload(CamelModel):
|
|
357
360
|
marker: int
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class ReadMessagesPayload(CamelModel):
|
|
364
|
+
type: ReadAction
|
|
365
|
+
chat_id: int
|
|
366
|
+
message_id: str
|
|
367
|
+
mark: int
|
pymax/protocols.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from logging import Logger
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
6
|
+
|
|
7
|
+
from pymax.payloads import UserAgentPayload
|
|
8
|
+
from pymax.static.constant import DEFAULT_TIMEOUT
|
|
9
|
+
from pymax.static.enum import Opcode
|
|
10
|
+
from pymax.types import (
|
|
11
|
+
Channel,
|
|
12
|
+
Chat,
|
|
13
|
+
Dialog,
|
|
14
|
+
Me,
|
|
15
|
+
Message,
|
|
16
|
+
ReactionInfo,
|
|
17
|
+
User,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import socket
|
|
22
|
+
import ssl
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from uuid import UUID
|
|
25
|
+
|
|
26
|
+
import websockets
|
|
27
|
+
|
|
28
|
+
from pymax.crud import Database
|
|
29
|
+
from pymax.filters import BaseFilter
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ClientProtocol(ABC):
|
|
33
|
+
def __init__(self, logger: Logger) -> None:
|
|
34
|
+
super().__init__()
|
|
35
|
+
self.logger = logger
|
|
36
|
+
self._users: dict[int, User] = {}
|
|
37
|
+
self.chats: list[Chat] = []
|
|
38
|
+
self._database: Database
|
|
39
|
+
self._device_id: UUID
|
|
40
|
+
self.uri: str
|
|
41
|
+
self.is_connected: bool = False
|
|
42
|
+
self.phone: str
|
|
43
|
+
self.dialogs: list[Dialog] = []
|
|
44
|
+
self.channels: list[Channel] = []
|
|
45
|
+
self.contacts: list[User] = []
|
|
46
|
+
self.me: Me | None = None
|
|
47
|
+
self.host: str
|
|
48
|
+
self.port: int
|
|
49
|
+
self.proxy: str | Literal[True] | None
|
|
50
|
+
self.registration: bool
|
|
51
|
+
self.first_name: str
|
|
52
|
+
self.last_name: str | None
|
|
53
|
+
self._token: str | None
|
|
54
|
+
self._work_dir: str
|
|
55
|
+
self.reconnect: bool
|
|
56
|
+
self.headers: UserAgentPayload
|
|
57
|
+
self._database_path: Path
|
|
58
|
+
self._ws: websockets.ClientConnection | None = None
|
|
59
|
+
self._seq: int = 0
|
|
60
|
+
self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
61
|
+
self._recv_task: asyncio.Task[Any] | None = None
|
|
62
|
+
self._incoming: asyncio.Queue[dict[str, Any]] | None = None
|
|
63
|
+
self._file_upload_waiters: dict[
|
|
64
|
+
int,
|
|
65
|
+
asyncio.Future[dict[str, Any]],
|
|
66
|
+
] = {}
|
|
67
|
+
self.user_agent = UserAgentPayload()
|
|
68
|
+
self._outgoing: asyncio.Queue[dict[str, Any]] | None = None
|
|
69
|
+
self._outgoing_task: asyncio.Task[Any] | None = None
|
|
70
|
+
self._error_count: int = 0
|
|
71
|
+
self._circuit_breaker: bool = False
|
|
72
|
+
self._last_error_time: float = 0.0
|
|
73
|
+
self._session_id: int
|
|
74
|
+
self._action_id: int = 0
|
|
75
|
+
self._current_screen: str = "chats_list_tab"
|
|
76
|
+
self._on_message_handlers: list[
|
|
77
|
+
tuple[Callable[[Message], Any], BaseFilter[Message] | None]
|
|
78
|
+
] = []
|
|
79
|
+
self._on_message_edit_handlers: list[
|
|
80
|
+
tuple[Callable[[Message], Any], BaseFilter[Message] | None]
|
|
81
|
+
] = []
|
|
82
|
+
self._on_message_delete_handlers: list[
|
|
83
|
+
tuple[Callable[[Message], Any], BaseFilter[Message] | None]
|
|
84
|
+
] = []
|
|
85
|
+
self._on_reaction_change_handlers: list[Callable[[str, int, ReactionInfo], Any]] = []
|
|
86
|
+
self._on_chat_update_handlers: list[Callable[[Chat], Any | Awaitable[Any]]] = []
|
|
87
|
+
self._on_raw_receive_handlers: list[Callable[[dict[str, Any]], Any | Awaitable[Any]]] = []
|
|
88
|
+
self._scheduled_tasks: list[tuple[Callable[[], Any | Awaitable[Any]], float]] = []
|
|
89
|
+
self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = None
|
|
90
|
+
self._background_tasks: set[asyncio.Task[Any]] = set()
|
|
91
|
+
self._ssl_context: ssl.SSLContext
|
|
92
|
+
self._socket: socket.socket | None = None
|
|
93
|
+
|
|
94
|
+
@abstractmethod
|
|
95
|
+
async def _send_and_wait(
|
|
96
|
+
self,
|
|
97
|
+
opcode: Opcode,
|
|
98
|
+
payload: dict[str, Any],
|
|
99
|
+
cmd: int = 0,
|
|
100
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
101
|
+
) -> dict[str, Any]:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
async def _get_chat(self, chat_id: int) -> Chat | None:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
async def _queue_message(
|
|
110
|
+
self,
|
|
111
|
+
opcode: int,
|
|
112
|
+
payload: dict[str, Any],
|
|
113
|
+
cmd: int = 0,
|
|
114
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
115
|
+
max_retries: int = 3,
|
|
116
|
+
) -> Message | None:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
@abstractmethod
|
|
120
|
+
def _create_safe_task(
|
|
121
|
+
self, coro: Awaitable[Any], name: str | None = None
|
|
122
|
+
) -> asyncio.Task[Any]:
|
|
123
|
+
pass
|
pymax/static/constant.py
CHANGED
|
@@ -1,10 +1,73 @@
|
|
|
1
|
+
from random import choice, randint
|
|
1
2
|
from re import Pattern, compile
|
|
2
3
|
from typing import Final
|
|
3
4
|
|
|
5
|
+
import ua_generator
|
|
4
6
|
from websockets.typing import Origin
|
|
5
7
|
|
|
8
|
+
from pymax.utils import MixinsUtils
|
|
9
|
+
|
|
10
|
+
DEVICE_NAMES: Final[list[str]] = [
|
|
11
|
+
"Chrome",
|
|
12
|
+
"Firefox",
|
|
13
|
+
"Edge",
|
|
14
|
+
"Safari",
|
|
15
|
+
"Opera",
|
|
16
|
+
"Vivaldi",
|
|
17
|
+
"Brave",
|
|
18
|
+
"Chromium",
|
|
19
|
+
# os
|
|
20
|
+
"Windows 10",
|
|
21
|
+
"Windows 11",
|
|
22
|
+
"macOS Big Sur",
|
|
23
|
+
"macOS Monterey",
|
|
24
|
+
"macOS Ventura",
|
|
25
|
+
"Ubuntu 20.04",
|
|
26
|
+
"Ubuntu 22.04",
|
|
27
|
+
"Fedora 35",
|
|
28
|
+
"Fedora 36",
|
|
29
|
+
"Debian 11",
|
|
30
|
+
]
|
|
31
|
+
SCREEN_SIZES: Final[list[str]] = [
|
|
32
|
+
"1920x1080 1.0x",
|
|
33
|
+
"1366x768 1.0x",
|
|
34
|
+
"1440x900 1.0x",
|
|
35
|
+
"1536x864 1.0x",
|
|
36
|
+
"1280x720 1.0x",
|
|
37
|
+
"1600x900 1.0x",
|
|
38
|
+
"1680x1050 1.0x",
|
|
39
|
+
"2560x1440 1.0x",
|
|
40
|
+
"3840x2160 1.0x",
|
|
41
|
+
]
|
|
42
|
+
OS_VERSIONS: Final[list[str]] = [
|
|
43
|
+
"Windows 10",
|
|
44
|
+
"Windows 11",
|
|
45
|
+
"macOS Big Sur",
|
|
46
|
+
"macOS Monterey",
|
|
47
|
+
"macOS Ventura",
|
|
48
|
+
"Ubuntu 20.04",
|
|
49
|
+
"Ubuntu 22.04",
|
|
50
|
+
"Fedora 35",
|
|
51
|
+
"Fedora 36",
|
|
52
|
+
"Debian 11",
|
|
53
|
+
]
|
|
54
|
+
TIMEZONES: Final[list[str]] = [
|
|
55
|
+
"Europe/Moscow",
|
|
56
|
+
"Europe/Kaliningrad",
|
|
57
|
+
"Europe/Samara",
|
|
58
|
+
"Asia/Yekaterinburg",
|
|
59
|
+
"Asia/Omsk",
|
|
60
|
+
"Asia/Krasnoyarsk",
|
|
61
|
+
"Asia/Irkutsk",
|
|
62
|
+
"Asia/Yakutsk",
|
|
63
|
+
"Asia/Vladivostok",
|
|
64
|
+
"Asia/Kamchatka",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
6
68
|
PHONE_REGEX: Final[Pattern[str]] = compile(r"^\+?\d{10,15}$")
|
|
7
69
|
WEBSOCKET_URI: Final[str] = "wss://ws-api.oneme.ru/websocket"
|
|
70
|
+
SESSION_STORAGE_DB = "session.db"
|
|
8
71
|
WEBSOCKET_ORIGIN: Final[Origin] = Origin("https://web.max.ru")
|
|
9
72
|
HOST: Final[str] = "api.oneme.ru"
|
|
10
73
|
PORT: Final[int] = 443
|
|
@@ -12,16 +75,14 @@ DEFAULT_TIMEOUT: Final[float] = 20.0
|
|
|
12
75
|
DEFAULT_DEVICE_TYPE: Final[str] = "DESKTOP"
|
|
13
76
|
DEFAULT_LOCALE: Final[str] = "ru"
|
|
14
77
|
DEFAULT_DEVICE_LOCALE: Final[str] = "ru"
|
|
15
|
-
DEFAULT_DEVICE_NAME: Final[str] =
|
|
16
|
-
DEFAULT_APP_VERSION: Final[str] = "25.12.
|
|
78
|
+
DEFAULT_DEVICE_NAME: Final[str] = choice(DEVICE_NAMES)
|
|
79
|
+
DEFAULT_APP_VERSION: Final[str] = "25.12.14"
|
|
17
80
|
DEFAULT_SCREEN: Final[str] = "1080x1920 1.0x"
|
|
18
|
-
DEFAULT_OS_VERSION: Final[str] =
|
|
19
|
-
DEFAULT_USER_AGENT: Final[str] = (
|
|
20
|
-
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
|
|
21
|
-
)
|
|
81
|
+
DEFAULT_OS_VERSION: Final[str] = choice(OS_VERSIONS)
|
|
82
|
+
DEFAULT_USER_AGENT: Final[str] = ua_generator.generate().text
|
|
22
83
|
DEFAULT_BUILD_NUMBER: Final[int] = 0x97CB
|
|
23
|
-
DEFAULT_CLIENT_SESSION_ID: Final[int] =
|
|
24
|
-
DEFAULT_TIMEZONE: Final[str] =
|
|
84
|
+
DEFAULT_CLIENT_SESSION_ID: Final[int] = randint(1, 15)
|
|
85
|
+
DEFAULT_TIMEZONE: Final[str] = choice(TIMEZONES)
|
|
25
86
|
DEFAULT_CHAT_MEMBERS_LIMIT: Final[int] = 50
|
|
26
87
|
DEFAULT_MARKER_VALUE: Final[int] = 0
|
|
27
88
|
DEFAULT_PING_INTERVAL: Final[float] = 30.0
|
pymax/static/enum.py
CHANGED
|
@@ -196,6 +196,7 @@ class AttachType(str, Enum):
|
|
|
196
196
|
STICKER = "STICKER"
|
|
197
197
|
AUDIO = "AUDIO"
|
|
198
198
|
CONTROL = "CONTROL"
|
|
199
|
+
CONTACT = "CONTACT"
|
|
199
200
|
|
|
200
201
|
|
|
201
202
|
class FormattingType(str, Enum):
|
|
@@ -215,3 +216,8 @@ class MarkupType(str, Enum):
|
|
|
215
216
|
class ContactAction(str, Enum):
|
|
216
217
|
ADD = "ADD"
|
|
217
218
|
REMOVE = "REMOVE"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class ReadAction(str, Enum):
|
|
222
|
+
READ_MESSAGE = "READ_MESSAGE"
|
|
223
|
+
READ_REACTION = "READ_REACTION"
|