maxapi-python 1.2.3__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.
pymax/mixins/websocket.py CHANGED
@@ -1,37 +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 LoginError, WebSocketNotConnectedError
11
- from pymax.filters import BaseFilter
12
- from pymax.interfaces import ClientProtocol
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 ChatType, MessageStatus, Opcode
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
- User,
31
19
  )
32
20
 
33
21
 
34
- class WebSocketMixin(ClientProtocol):
22
+ class WebSocketMixin(BaseTransport):
35
23
  @property
36
24
  def ws(self) -> websockets.ClientConnection:
37
25
  if self._ws is None or not self.is_connected:
@@ -39,34 +27,6 @@ class WebSocketMixin(ClientProtocol):
39
27
  raise WebSocketNotConnectedError
40
28
  return self._ws
41
29
 
42
- def _make_message(
43
- self, opcode: Opcode, payload: dict[str, Any], cmd: int = 0
44
- ) -> dict[str, Any]:
45
- self._seq += 1
46
-
47
- msg = BaseWebSocketMessage(
48
- cmd=cmd,
49
- seq=self._seq,
50
- opcode=opcode.value,
51
- payload=payload,
52
- ).model_dump(by_alias=True)
53
-
54
- self.logger.debug("make_message opcode=%s cmd=%s seq=%s", opcode, cmd, self._seq)
55
- return msg
56
-
57
- async def _send_interactive_ping(self) -> None:
58
- while self.is_connected:
59
- try:
60
- await self._send_and_wait(
61
- opcode=Opcode.PING,
62
- payload={"interactive": True},
63
- cmd=0,
64
- )
65
- self.logger.debug("Interactive ping sent successfully")
66
- except Exception:
67
- self.logger.warning("Interactive ping failed", exc_info=True)
68
- await asyncio.sleep(DEFAULT_PING_INTERVAL)
69
-
70
30
  async def connect(self, user_agent: UserAgentPayload | None = None) -> dict[str, Any] | None:
71
31
  """
72
32
  Устанавливает соединение WebSocket с сервером и выполняет handshake.
@@ -100,159 +60,6 @@ class WebSocketMixin(ClientProtocol):
100
60
  self.logger.info("WebSocket connected, starting handshake")
101
61
  return await self._handshake(user_agent)
102
62
 
103
- async def _handshake(self, user_agent: UserAgentPayload) -> dict[str, Any]:
104
- self.logger.debug(
105
- "Sending handshake with user_agent keys=%s",
106
- user_agent.model_dump(by_alias=True).keys(),
107
- )
108
-
109
- user_agent_json = user_agent.model_dump(by_alias=True)
110
- resp = await self._send_and_wait(
111
- opcode=Opcode.SESSION_INIT,
112
- payload={"deviceId": str(self._device_id), "userAgent": user_agent_json},
113
- )
114
-
115
- if resp.get("payload", {}).get("error"):
116
- MixinsUtils.handle_error(resp)
117
-
118
- self.logger.info("Handshake completed")
119
- return resp
120
-
121
- async def _process_message_handler(
122
- self,
123
- handler: Callable[[Message], Any],
124
- filter: BaseFilter[Message] | None,
125
- message: Message,
126
- ):
127
- result = None
128
- if filter:
129
- if filter(message):
130
- result = handler(message)
131
- else:
132
- return
133
- else:
134
- result = handler(message)
135
- if asyncio.iscoroutine(result):
136
- self._create_safe_task(result, name=f"handler-{handler.__name__}")
137
-
138
- def _parse_json(self, raw: Any) -> dict[str, Any] | None:
139
- try:
140
- return json.loads(raw)
141
- except Exception:
142
- self.logger.warning("JSON parse error", exc_info=True)
143
- return None
144
-
145
- def _handle_pending(self, seq: int | None, data: dict) -> bool:
146
- if isinstance(seq, int):
147
- fut = self._pending.get(seq)
148
- if fut and not fut.done():
149
- fut.set_result(data)
150
- self.logger.debug("Matched response for pending seq=%s", seq)
151
- return True
152
- return False
153
-
154
- async def _handle_incoming_queue(self, data: dict[str, Any]) -> None:
155
- if self._incoming:
156
- try:
157
- self._incoming.put_nowait(data)
158
- except asyncio.QueueFull:
159
- self.logger.warning(
160
- "Incoming queue full; dropping message seq=%s", data.get("seq")
161
- )
162
-
163
- async def _handle_file_upload(self, data: dict[str, Any]) -> None:
164
- if data.get("opcode") != Opcode.NOTIF_ATTACH:
165
- return
166
- payload = data.get("payload", {})
167
- for key in ("fileId", "videoId"):
168
- id_ = payload.get(key)
169
- if id_ is not None:
170
- fut = self._file_upload_waiters.pop(id_, None)
171
- if fut and not fut.done():
172
- fut.set_result(data)
173
- self.logger.debug("Fulfilled file upload waiter for %s=%s", key, id_)
174
-
175
- async def _handle_message_notifications(self, data: dict) -> None:
176
- if data.get("opcode") != Opcode.NOTIF_MESSAGE.value:
177
- return
178
- payload = data.get("payload", {})
179
- msg = Message.from_dict(payload)
180
- if not msg:
181
- return
182
- handlers_map = {
183
- MessageStatus.EDITED: self._on_message_edit_handlers,
184
- MessageStatus.REMOVED: self._on_message_delete_handlers,
185
- }
186
- if msg.status and msg.status in handlers_map:
187
- for handler, filter in handlers_map[msg.status]:
188
- await self._process_message_handler(handler, filter, msg)
189
- if msg.status is None:
190
- for handler, filter in self._on_message_handlers:
191
- await self._process_message_handler(handler, filter, msg)
192
-
193
- async def _handle_reactions(self, data: dict):
194
- if data.get("opcode") != Opcode.NOTIF_MSG_REACTIONS_CHANGED:
195
- return
196
-
197
- payload = data.get("payload", {})
198
- chat_id = payload.get("chatId")
199
- message_id = payload.get("messageId")
200
-
201
- if not (chat_id and message_id):
202
- return
203
-
204
- total_count = payload.get("totalCount")
205
- your_reaction = payload.get("yourReaction")
206
- counters = [ReactionCounter.from_dict(c) for c in payload.get("counters", [])]
207
-
208
- reaction_info = ReactionInfo(
209
- total_count=total_count,
210
- your_reaction=your_reaction,
211
- counters=counters,
212
- )
213
-
214
- for handler in self._on_reaction_change_handlers:
215
- try:
216
- result = handler(message_id, chat_id, reaction_info)
217
- if asyncio.iscoroutine(result):
218
- await result
219
- except Exception as e:
220
- self.logger.exception("Error in on_reaction_change_handler: %s", e)
221
-
222
- async def _handle_chat_updates(self, data: dict) -> None:
223
- if data.get("opcode") != Opcode.NOTIF_CHAT:
224
- return
225
-
226
- payload = data.get("payload", {})
227
- chat_data = payload.get("chat", {})
228
- chat = Chat.from_dict(chat_data)
229
- if not chat:
230
- return
231
-
232
- for handler in self._on_chat_update_handlers:
233
- try:
234
- result = handler(chat)
235
- if asyncio.iscoroutine(result):
236
- await result
237
- except Exception as e:
238
- self.logger.exception("Error in on_chat_update_handler: %s", e)
239
-
240
- async def _handle_raw_receive(self, data: dict[str, Any]) -> None:
241
- for handler in self._on_raw_receive_handlers:
242
- try:
243
- result = handler(data)
244
- if asyncio.iscoroutine(result):
245
- await result
246
- except Exception as e:
247
- self.logger.exception("Error in on_raw_receive_handler: %s", e)
248
-
249
- async def _dispatch_incoming(self, data: dict[str, Any]) -> None:
250
- await self._handle_raw_receive(data)
251
- await self._handle_file_upload(data)
252
- await self._handle_message_notifications(data)
253
- await self._handle_reactions(data)
254
- await self._handle_chat_updates(data)
255
-
256
63
  async def _recv_loop(self) -> None:
257
64
  if self._ws is None:
258
65
  self.logger.warning("Recv loop started without websocket instance")
@@ -292,38 +99,6 @@ class WebSocketMixin(ClientProtocol):
292
99
  self.logger.exception("Error in recv_loop; backing off briefly")
293
100
  await asyncio.sleep(RECV_LOOP_BACKOFF_DELAY)
294
101
 
295
- def _log_task_exception(self, fut: asyncio.Future[Any]) -> None:
296
- try:
297
- fut.result()
298
- except asyncio.CancelledError:
299
- pass
300
- except Exception as e:
301
- self.logger.exception("Error retrieving task exception: %s", e)
302
-
303
- async def _queue_message(
304
- self,
305
- opcode: int,
306
- payload: dict[str, Any],
307
- cmd: int = 0,
308
- timeout: float = DEFAULT_TIMEOUT,
309
- max_retries: int = 3,
310
- ) -> None:
311
- if self._outgoing is None:
312
- self.logger.warning("Outgoing queue not initialized")
313
- return
314
-
315
- message = {
316
- "opcode": opcode,
317
- "payload": payload,
318
- "cmd": cmd,
319
- "timeout": timeout,
320
- "retry_count": 0,
321
- "max_retries": max_retries,
322
- }
323
-
324
- await self._outgoing.put(message)
325
- self.logger.debug("Message queued for sending")
326
-
327
102
  @override
328
103
  async def _send_and_wait(
329
104
  self,
@@ -359,139 +134,6 @@ class WebSocketMixin(ClientProtocol):
359
134
  finally:
360
135
  self._pending.pop(msg["seq"], None)
361
136
 
362
- async def _outgoing_loop(self) -> None:
363
- while self.is_connected:
364
- try:
365
- if self._outgoing is None:
366
- await asyncio.sleep(0.1)
367
- continue
368
-
369
- if self._circuit_breaker:
370
- if time.time() - self._last_error_time > 60:
371
- self._circuit_breaker = False
372
- self._error_count = 0
373
- self.logger.info("Circuit breaker reset")
374
- else:
375
- await asyncio.sleep(5)
376
- continue
377
-
378
- message = await self._outgoing.get() # TODO: persistent msg q mb?
379
- if not message:
380
- continue
381
-
382
- retry_count = message.get("retry_count", 0)
383
- max_retries = message.get("max_retries", 3)
384
-
385
- try:
386
- await self._send_and_wait(
387
- opcode=message["opcode"],
388
- payload=message["payload"],
389
- cmd=message.get("cmd", 0),
390
- timeout=message.get("timeout", DEFAULT_TIMEOUT),
391
- )
392
- self.logger.debug("Message sent successfully from queue")
393
- self._error_count = max(0, self._error_count - 1)
394
- except Exception as e:
395
- self._error_count += 1
396
- self._last_error_time = time.time()
397
-
398
- if self._error_count > 10:
399
- self._circuit_breaker = True
400
- self.logger.warning(
401
- "Circuit breaker activated due to %d consecutive errors",
402
- self._error_count,
403
- )
404
- await self._outgoing.put(message)
405
- continue
406
-
407
- retry_delay = self._get_retry_delay(e, retry_count)
408
- self.logger.warning(
409
- "Failed to send message from queue: %s (delay: %ds)",
410
- e,
411
- retry_delay,
412
- )
413
-
414
- if retry_count < max_retries:
415
- message["retry_count"] = retry_count + 1
416
- await asyncio.sleep(retry_delay)
417
- await self._outgoing.put(message)
418
- else:
419
- self.logger.error(
420
- "Message failed after %d retries, dropping",
421
- max_retries,
422
- )
423
-
424
- except Exception:
425
- self.logger.exception("Error in outgoing loop")
426
- await asyncio.sleep(1)
427
-
428
- def _get_retry_delay(self, error: Exception, retry_count: int) -> float:
429
- if isinstance(error, (ConnectionError, OSError)):
430
- return 1.0
431
- elif isinstance(error, TimeoutError):
432
- return 5.0
433
- elif isinstance(error, WebSocketNotConnectedError):
434
- return 2.0
435
- else:
436
- return float(2**retry_count)
437
-
438
- async def _sync(self, user_agent: UserAgentPayload) -> None:
439
- self.logger.info("Starting initial sync")
440
-
441
- payload = SyncPayload(
442
- interactive=True,
443
- token=self._token,
444
- chats_sync=0,
445
- contacts_sync=0,
446
- presence_sync=0,
447
- drafts_sync=0,
448
- chats_count=40,
449
- user_agent=user_agent,
450
- ).model_dump(by_alias=True)
451
- try:
452
- data = await self._send_and_wait(opcode=Opcode.LOGIN, payload=payload)
453
- raw_payload = data.get("payload", {})
454
-
455
- if error := raw_payload.get("error"):
456
- MixinsUtils.handle_error(data)
457
-
458
- for raw_chat in raw_payload.get("chats", []):
459
- try:
460
- if raw_chat.get("type") == ChatType.DIALOG.value:
461
- self.dialogs.append(Dialog.from_dict(raw_chat))
462
- elif raw_chat.get("type") == ChatType.CHAT.value:
463
- self.chats.append(Chat.from_dict(raw_chat))
464
- elif raw_chat.get("type") == ChatType.CHANNEL.value:
465
- self.channels.append(Channel.from_dict(raw_chat))
466
- except Exception:
467
- self.logger.exception("Error parsing chat entry")
468
-
469
- for raw_user in raw_payload.get("contacts", []):
470
- try:
471
- user = User.from_dict(raw_user)
472
- if user:
473
- self.contacts.append(user)
474
- except Exception:
475
- self.logger.exception("Error parsing contact entry")
476
-
477
- if raw_payload.get("profile", {}).get("contact"):
478
- self.me = Me.from_dict(raw_payload.get("profile", {}).get("contact", {}))
479
-
480
- self.logger.info(
481
- "Sync completed: dialogs=%d chats=%d channels=%d",
482
- len(self.dialogs),
483
- len(self.chats),
484
- len(self.channels),
485
- )
486
-
487
- except Exception as e:
488
- self.logger.exception("Sync failed")
489
- self.is_connected = False
490
- if self._ws:
491
- await self._ws.close()
492
- self._ws = None
493
- raise e
494
-
495
137
  @override
496
138
  async def _get_chat(self, chat_id: int) -> Chat | None:
497
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:
@@ -358,3 +358,10 @@ class LeaveChatPayload(CamelModel):
358
358
 
359
359
  class FetchChatsPayload(CamelModel):
360
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] = "Chrome"
16
- DEFAULT_APP_VERSION: Final[str] = "25.12.13"
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] = "Linux"
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] = 14
24
- DEFAULT_TIMEZONE: Final[str] = "Europe/Moscow"
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
@@ -216,3 +216,8 @@ class MarkupType(str, Enum):
216
216
  class ContactAction(str, Enum):
217
217
  ADD = "ADD"
218
218
  REMOVE = "REMOVE"
219
+
220
+
221
+ class ReadAction(str, Enum):
222
+ READ_MESSAGE = "READ_MESSAGE"
223
+ READ_REACTION = "READ_REACTION"
pymax/types.py CHANGED
@@ -1193,3 +1193,28 @@ class FolderList:
1193
1193
  @override
1194
1194
  def __str__(self) -> str:
1195
1195
  return f"FolderList: {len(self.folders)} folders"
1196
+
1197
+
1198
+ class ReadState:
1199
+ def __init__(
1200
+ self,
1201
+ unread: int,
1202
+ mark: int,
1203
+ ) -> None:
1204
+ self.unread = unread
1205
+ self.mark = mark
1206
+
1207
+ @classmethod
1208
+ def from_dict(cls, data: dict[str, Any]) -> Self:
1209
+ return cls(
1210
+ unread=data["unread"],
1211
+ mark=data["mark"],
1212
+ )
1213
+
1214
+ @override
1215
+ def __repr__(self) -> str:
1216
+ return f"ReadState(unread={self.unread!r}, mark={self.mark!r})"
1217
+
1218
+ @override
1219
+ def __str__(self) -> str:
1220
+ return f"ReadState: unread={self.unread}, mark={self.mark}"