maxapi-python 0.1.3__py3-none-any.whl → 1.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxapi-python
3
- Version: 0.1.3
3
+ Version: 1.1.1
4
4
  Summary: Python wrapper для API мессенджера Max
5
5
  Project-URL: Homepage, https://github.com/noxzion/PyMax
6
6
  Project-URL: Repository, https://github.com/noxzion/PyMax
@@ -0,0 +1,28 @@
1
+ pymax/__init__.py,sha256=Wu5eniruWbotDUHBymdAyDNzt4g40r7Lb2pb9usQgjU,939
2
+ pymax/core.py,sha256=cayfz0kmHSLou_i7IQLFRCnNVg48e5BfmPFny7Db8c4,7785
3
+ pymax/crud.py,sha256=Mk-c87GItS91BlJu6INDbw1-ovXyoB2D9rXHK8voxpU,3207
4
+ pymax/exceptions.py,sha256=msS11MD7qZPm0qZ6O8fobTm-GTldm2IA3uQLTX6eDxc,919
5
+ pymax/files.py,sha256=Tpv-43gS7I4Pwlaimb8mZ2B-ZkF3aMsrLYT20NPaqhE,2656
6
+ pymax/filters.py,sha256=EejNuJMmSBhw3bUqDoqXEnCnLjGy_sw5aH3Vynpxc0A,1306
7
+ pymax/interfaces.py,sha256=_D6iMQI74Gdtl6-HMoE1acRFRHeITgOZPVFpt5pvoow,2394
8
+ pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
9
+ pymax/navigation.py,sha256=16c1_FZrw24uFlP6W5-F8OrEQE73bkQA3HSFqTdBtgo,5725
10
+ pymax/payloads.py,sha256=yeBRxiMq6ixUQjMBBFcBDtBpYzqqfaEII3Z1kJq8pe8,3907
11
+ pymax/static.py,sha256=wwSV1ue5s5buqWz6TvCzjzN2ZWI-wITposTRvcS151g,4738
12
+ pymax/types.py,sha256=SFjCG9GlFtJBDydd6pWtzFbJ6kTADZlxBIbKKRk7E1A,17820
13
+ pymax/utils.py,sha256=F2TdoWfSwDLeh2uIcMIE_GTdXd7hU7gWti2i5P727bA,1364
14
+ pymax/mixins/__init__.py,sha256=-PSMwTVioS-VTy-EGfV-epaKFLy58R4N2b-rX6wJf-M,649
15
+ pymax/mixins/auth.py,sha256=vTNSZ6AunvDIMPQAvgYozpIZaCWMYiMDiabCBI7Sm6c,3079
16
+ pymax/mixins/channel.py,sha256=Stnf63GPtlQnsMPVEC9P0oardEOz50I4DCXN5H5s1SM,823
17
+ pymax/mixins/group.py,sha256=QJCd5MLYCVRrClcuAuRkLV3oylJRAOaGw0xUqFm2uXk,7820
18
+ pymax/mixins/handler.py,sha256=I1iNPaEgpvFnphaxV6liLwVaBCJ8sN6-h7908-_tPFk,2104
19
+ pymax/mixins/message.py,sha256=0y7fO61zg9XG46SKxOPCOLMJW1LJBWfTi-PR3HjJfJo,10249
20
+ pymax/mixins/self.py,sha256=V0gbkY3jfX9fnd7v06n4_s7P3HTPcS1KNPqUzA0vNi8,1169
21
+ pymax/mixins/socket.py,sha256=sgGxX7FmLyO3M82Nu2L-n00dCn0W2fV5TnrCipiKhaE,15738
22
+ pymax/mixins/telemetry.py,sha256=0sQl6kvFVxobLthNAPNS9LzMrwwzZFA1xmOnvfiHWos,3522
23
+ pymax/mixins/user.py,sha256=U-epgvLruTDHBCrLDE0N0iWeOypGE1_SU8cKD3TE90U,3045
24
+ pymax/mixins/websocket.py,sha256=ndgcezJ1xqYCpaxc17rXxiXDQrTwTmtrgvx_d1H2Pcc,10094
25
+ maxapi_python-1.1.1.dist-info/METADATA,sha256=kwB6ryURXiw-Cyf59AUQFyqUEKSSc5RVK34_umD7VYY,5930
26
+ maxapi_python-1.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
+ maxapi_python-1.1.1.dist-info/licenses/LICENSE,sha256=oe-AGp86WMKawV4KmqF28Q0m-kGAhPfAOPrEUm4MnVw,1064
28
+ maxapi_python-1.1.1.dist-info/RECORD,,
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 noxzion
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2025 noxzion
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pymax/__init__.py CHANGED
@@ -1,55 +1,57 @@
1
- """
2
- Python wrapper для API мессенджера Max
3
- """
4
-
5
- from .core import (
6
- InvalidPhoneError,
7
- MaxClient,
8
- WebSocketNotConnectedError,
9
- )
10
- from .static import (
11
- AccessType,
12
- AuthType,
13
- ChatType,
14
- Constants,
15
- DeviceType,
16
- ElementType,
17
- MessageStatus,
18
- MessageType,
19
- Opcode,
20
- )
21
- from .types import (
22
- Channel,
23
- Chat,
24
- Dialog,
25
- Element,
26
- Message,
27
- User,
28
- )
29
-
30
- __author__ = "noxzion"
31
-
32
- __all__ = [
33
- # Перечисления и константы
34
- "AccessType",
35
- "AuthType",
36
- # Типы данных
37
- "Channel",
38
- "Chat",
39
- "ChatType",
40
- "Constants",
41
- "DeviceType",
42
- "Dialog",
43
- "Element",
44
- "ElementType",
45
- # Исключения
46
- "InvalidPhoneError",
47
- # Клиент
48
- "MaxClient",
49
- "Message",
50
- "MessageStatus",
51
- "MessageType",
52
- "Opcode",
53
- "User",
54
- "WebSocketNotConnectedError",
55
- ]
1
+ """
2
+ Python wrapper для API мессенджера Max
3
+ """
4
+
5
+ from .core import (
6
+ InvalidPhoneError,
7
+ MaxClient,
8
+ SocketMaxClient,
9
+ WebSocketNotConnectedError,
10
+ )
11
+ from .static import (
12
+ AccessType,
13
+ AuthType,
14
+ ChatType,
15
+ Constants,
16
+ DeviceType,
17
+ ElementType,
18
+ MessageStatus,
19
+ MessageType,
20
+ Opcode,
21
+ )
22
+ from .types import (
23
+ Channel,
24
+ Chat,
25
+ Dialog,
26
+ Element,
27
+ Message,
28
+ User,
29
+ )
30
+
31
+ __author__ = "noxzion"
32
+
33
+ __all__ = [
34
+ # Перечисления и константы
35
+ "AccessType",
36
+ "AuthType",
37
+ # Типы данных
38
+ "Channel",
39
+ "Chat",
40
+ "ChatType",
41
+ "Constants",
42
+ "DeviceType",
43
+ "Dialog",
44
+ "Element",
45
+ "ElementType",
46
+ # Исключения
47
+ "InvalidPhoneError",
48
+ # Клиент
49
+ "MaxClient",
50
+ "Message",
51
+ "MessageStatus",
52
+ "MessageType",
53
+ "Opcode",
54
+ "SocketMaxClient",
55
+ "User",
56
+ "WebSocketNotConnectedError",
57
+ ]
pymax/core.py CHANGED
@@ -1,156 +1,193 @@
1
- import asyncio
2
- import json
3
- import logging
4
- import re
5
- import time
6
- from collections.abc import Awaitable, Callable
7
- from pathlib import Path
8
- from typing import TYPE_CHECKING, Any
9
-
10
- import websockets
11
-
12
- from .crud import Database
13
- from .exceptions import InvalidPhoneError, WebSocketNotConnectedError
14
- from .mixins import ApiMixin, WebSocketMixin
15
- from .payloads import (
16
- BaseWebSocketMessage,
17
- SyncPayload,
18
- )
19
- from .static import ChatType, Constants, Opcode
20
- from .types import Channel, Chat, Dialog, Me, Message, User, override
21
-
22
- if TYPE_CHECKING:
23
- from .filters import Filter
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
-
28
- class MaxClient(ApiMixin, WebSocketMixin):
29
- """
30
- Основной клиент для работы с WebSocket API сервиса Max.
31
-
32
-
33
- Args:
34
- phone (str): Номер телефона для авторизации.
35
- uri (str, optional): URI WebSocket сервера. По умолчанию Constants.WEBSOCKET_URI.value.
36
- work_dir (str, optional): Рабочая директория для хранения базы данных. По умолчанию ".".
37
- logger (logging.Logger | None): Пользовательский логгер. Если не передан используется
38
- логгер модуля с именем f"{__name__}.MaxClient".
39
-
40
- Raises:
41
- InvalidPhoneError: Если формат номера телефона неверный.
42
- """
43
-
44
- def __init__(
45
- self,
46
- phone: str,
47
- uri: str = Constants.WEBSOCKET_URI.value,
48
- headers: dict[str, Any] | None = Constants.DEFAULT_USER_AGENT.value,
49
- token: str | None = None,
50
- work_dir: str = ".",
51
- logger: logging.Logger | None = None,
52
- ) -> None:
53
- self.uri: str = uri
54
- self.is_connected: bool = False
55
- self.phone: str = phone
56
- self.chats: list[Chat] = []
57
- self.dialogs: list[Dialog] = []
58
- self.channels: list[Channel] = []
59
- self.me: Me | None = None
60
- self._users: dict[int, User] = {}
61
- if not self._check_phone():
62
- raise InvalidPhoneError(self.phone)
63
- self._work_dir: str = work_dir
64
- self._database_path: Path = Path(work_dir) / "session.db"
65
- self._database_path.parent.mkdir(parents=True, exist_ok=True)
66
- self._database_path.touch(exist_ok=True)
67
- self._database = Database(self._work_dir)
68
- self._ws: websockets.ClientConnection | None = None
69
- self._seq: int = 0
70
- self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
71
- self._recv_task: asyncio.Task[Any] | None = None
72
- self._incoming: asyncio.Queue[dict[str, Any]] | None = None
73
- self._device_id = self._database.get_device_id()
74
- self._token = self._database.get_auth_token() or token
75
- self.user_agent = headers
76
- self._on_message_handlers: list[
77
- tuple[Callable[[Message], Any], Filter | None]
78
- ] = []
79
- self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = None
80
- self._background_tasks: set[asyncio.Task[Any]] = set()
81
- self.logger = logger or logging.getLogger(f"{__name__}.MaxClient")
82
- self._setup_logger()
83
-
84
- self.logger.debug(
85
- "Initialized MaxClient uri=%s work_dir=%s", self.uri, self._work_dir
86
- )
87
-
88
- def _setup_logger(self) -> None:
89
- self.logger.setLevel(logging.INFO)
90
-
91
- if not logger.handlers:
92
- handler = logging.StreamHandler()
93
- formatter = logging.Formatter(
94
- "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
95
- )
96
- handler.setFormatter(formatter)
97
- logger.addHandler(handler)
98
-
99
- async def close(self) -> None:
100
- try:
101
- self.logger.info("Closing client")
102
- if self._recv_task:
103
- self._recv_task.cancel()
104
- try:
105
- await self._recv_task
106
- except asyncio.CancelledError:
107
- self.logger.debug("recv_task cancelled")
108
- if self._ws:
109
- await self._ws.close()
110
- self.is_connected = False
111
- self.logger.info("Client closed")
112
- except Exception:
113
- self.logger.exception("Error closing client")
114
-
115
- async def start(self) -> None:
116
- """
117
- Запускает клиент, подключается к WebSocket, авторизует
118
- пользователя (если нужно) и запускает фоновый цикл.
119
- """
120
- try:
121
- self.logger.info("Client starting")
122
- await self._connect(self.user_agent)
123
-
124
- if self._token and self._database.get_auth_token() is None:
125
- self._database.update_auth_token(self._device_id, self._token)
126
-
127
- if self._token is None:
128
- await self._login()
129
- else:
130
- await self._sync()
131
-
132
- if self._on_start_handler:
133
- self.logger.debug("Calling on_start handler")
134
- result = self._on_start_handler()
135
- if asyncio.iscoroutine(result):
136
- await result
137
-
138
- if self._ws:
139
- ping_task = asyncio.create_task(self._send_interactive_ping())
140
- self._background_tasks.add(ping_task)
141
- ping_task.add_done_callback(
142
- lambda t: self._background_tasks.discard(t)
143
- or self._log_task_exception(t)
144
- )
145
-
146
- try:
147
- await self._ws.wait_closed()
148
- except asyncio.CancelledError:
149
- self.logger.debug("wait_closed cancelled")
150
- except Exception:
151
- self.logger.exception("Client start failed")
152
-
153
-
154
- class SocketMaxClient:
155
- pass # нокс займись
156
- # нет не займусь
1
+ import asyncio
2
+ import logging
3
+ import socket
4
+ import ssl
5
+ import time
6
+ from collections.abc import Awaitable, Callable
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from typing_extensions import override
11
+
12
+ from .crud import Database
13
+ from .exceptions import InvalidPhoneError
14
+ from .mixins import ApiMixin, SocketMixin, WebSocketMixin
15
+ from .static import Constants
16
+ from .types import Channel, Chat, Dialog, Me, Message, User
17
+
18
+ if TYPE_CHECKING:
19
+ import websockets
20
+
21
+ from .filters import Filter
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class MaxClient(ApiMixin, WebSocketMixin):
27
+ """
28
+ Основной клиент для работы с WebSocket API сервиса Max.
29
+
30
+
31
+ Args:
32
+ phone (str): Номер телефона для авторизации.
33
+ uri (str, optional): URI WebSocket сервера. По умолчанию Constants.WEBSOCKET_URI.value.
34
+ work_dir (str, optional): Рабочая директория для хранения базы данных. По умолчанию ".".
35
+ logger (logging.Logger | None): Пользовательский логгер. Если не передан — используется
36
+ логгер модуля с именем f"{__name__}.MaxClient".
37
+ headers (dict[str, Any] | None): Заголовки для подключения к WebSocket. По умолчанию
38
+ Constants.DEFAULT_USER_AGENT.value.
39
+ token (str | None, optional): Токен авторизации. Если не передан, будет выполнен
40
+ процесс логина по номеру телефона.
41
+ host (str, optional): Хост API сервера. По умолчанию Constants.HOST.value.
42
+ port (int, optional): Порт API сервера. По умолчанию Constants.PORT.value.
43
+
44
+ Raises:
45
+ InvalidPhoneError: Если формат номера телефона неверный.
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ phone: str,
51
+ uri: str = Constants.WEBSOCKET_URI.value,
52
+ headers: dict[str, Any] | None = Constants.DEFAULT_USER_AGENT.value,
53
+ token: str | None = None,
54
+ send_fake_telemetry: bool = True,
55
+ host: str = Constants.HOST.value,
56
+ port: int = Constants.PORT.value,
57
+ work_dir: str = ".",
58
+ logger: logging.Logger | None = None,
59
+ ) -> None:
60
+ self.uri: str = uri
61
+ self.is_connected: bool = False
62
+ self.phone: str = phone
63
+ self.chats: list[Chat] = []
64
+ self.dialogs: list[Dialog] = []
65
+ self.channels: list[Channel] = []
66
+ self.me: Me | None = None
67
+ self._users: dict[int, User] = {}
68
+ if not self._check_phone():
69
+ raise InvalidPhoneError(self.phone)
70
+ self.host: str = host
71
+ self.port: int = port
72
+ self._work_dir: str = work_dir
73
+ self._database_path: Path = Path(work_dir) / "session.db"
74
+ self._database_path.parent.mkdir(parents=True, exist_ok=True)
75
+ self._database_path.touch(exist_ok=True)
76
+ self._database = Database(self._work_dir)
77
+ self._ws: websockets.ClientConnection | None = None
78
+ self._seq: int = 0
79
+ self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
80
+ self._recv_task: asyncio.Task[Any] | None = None
81
+ self._incoming: asyncio.Queue[dict[str, Any]] | None = None
82
+ self._device_id = self._database.get_device_id()
83
+ self._token = self._database.get_auth_token() or token
84
+ self.user_agent = headers
85
+
86
+ self._send_fake_telemetry: bool = send_fake_telemetry
87
+ self._session_id: int = int(time.time() * 1000)
88
+ self._action_id: int = 1
89
+ self._current_screen: str = "chats_list_tab"
90
+
91
+ self._on_message_handlers: list[
92
+ tuple[Callable[[Message], Any], Filter | None]
93
+ ] = []
94
+ self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = None
95
+ self._background_tasks: set[asyncio.Task[Any]] = set()
96
+ self._ssl_context = ssl.create_default_context()
97
+ self._ssl_context.set_ciphers("DEFAULT")
98
+ self._ssl_context.check_hostname = True
99
+ self._ssl_context.verify_mode = ssl.CERT_REQUIRED
100
+ self._ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
101
+ self._ssl_context.load_default_certs()
102
+ self._socket: socket.socket | None = None
103
+ self.logger = logger or logging.getLogger(f"{__name__}.MaxClient")
104
+ self._setup_logger()
105
+
106
+ self.logger.debug(
107
+ "Initialized MaxClient uri=%s work_dir=%s", self.uri, self._work_dir
108
+ )
109
+
110
+ def _setup_logger(self) -> None:
111
+ self.logger.setLevel(logging.INFO)
112
+
113
+ if not logger.handlers:
114
+ handler = logging.StreamHandler()
115
+ formatter = logging.Formatter(
116
+ "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
117
+ )
118
+ handler.setFormatter(formatter)
119
+ logger.addHandler(handler)
120
+
121
+ async def _wait_forever(self):
122
+ try:
123
+ await self.ws.wait_closed()
124
+ except asyncio.CancelledError:
125
+ self.logger.debug("wait_closed cancelled")
126
+
127
+ async def close(self) -> None:
128
+ try:
129
+ self.logger.info("Closing client")
130
+ if self._recv_task:
131
+ self._recv_task.cancel()
132
+ try:
133
+ await self._recv_task
134
+ except asyncio.CancelledError:
135
+ self.logger.debug("recv_task cancelled")
136
+ if self._ws:
137
+ await self._ws.close()
138
+ self.is_connected = False
139
+ self.logger.info("Client closed")
140
+ except Exception:
141
+ self.logger.exception("Error closing client")
142
+
143
+ async def start(self) -> None:
144
+ """
145
+ Запускает клиент, подключается к WebSocket, авторизует
146
+ пользователя (если нужно) и запускает фоновый цикл.
147
+ """
148
+ try:
149
+ self.logger.info("Client starting")
150
+ await self._connect(self.user_agent)
151
+
152
+ if self._token and self._database.get_auth_token() is None:
153
+ self._database.update_auth_token(self._device_id, self._token)
154
+
155
+ if self._token is None:
156
+ await self._login()
157
+ else:
158
+ await self._sync()
159
+
160
+ if self._on_start_handler:
161
+ self.logger.debug("Calling on_start handler")
162
+ result = self._on_start_handler()
163
+ if asyncio.iscoroutine(result):
164
+ await result
165
+
166
+ ping_task = asyncio.create_task(self._send_interactive_ping())
167
+ self._background_tasks.add(ping_task)
168
+ if self._send_fake_telemetry:
169
+ telemetry_task = asyncio.create_task(self._start())
170
+ self._background_tasks.add(telemetry_task)
171
+ telemetry_task.add_done_callback(
172
+ lambda t: self._background_tasks.discard(t)
173
+ or self._log_task_exception(t)
174
+ )
175
+ ping_task.add_done_callback(
176
+ lambda t: self._background_tasks.discard(t)
177
+ or self._log_task_exception(t)
178
+ )
179
+ await self._wait_forever()
180
+ except Exception:
181
+ self.logger.exception("Client start failed")
182
+
183
+
184
+ class SocketMaxClient(SocketMixin, MaxClient):
185
+ @override
186
+ async def _wait_forever(self):
187
+ if self._recv_task:
188
+ try:
189
+ await self._recv_task
190
+ except asyncio.CancelledError:
191
+ self.logger.debug("Socket recv_task cancelled")
192
+ except Exception as e:
193
+ self.logger.exception("Socket recv_task failed: %s", e)