maxapi-python 1.2.1__py3-none-any.whl → 1.2.2__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: 1.2.1
3
+ Version: 1.2.2
4
4
  Summary: Python wrapper для API мессенджера Max
5
5
  Project-URL: Homepage, https://github.com/ink-developer/PyMax
6
6
  Project-URL: Repository, https://github.com/ink-developer/PyMax
@@ -19,6 +19,13 @@ Requires-Dist: msgpack>=1.1.1
19
19
  Requires-Dist: qrcode>=8.2
20
20
  Requires-Dist: sqlmodel>=0.0.24
21
21
  Requires-Dist: websockets>=15.0
22
+ Provides-Extra: test
23
+ Requires-Dist: flake8; extra == 'test'
24
+ Requires-Dist: mypy; extra == 'test'
25
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == 'test'
26
+ Requires-Dist: pytest-cov>=5.0.0; extra == 'test'
27
+ Requires-Dist: pytest-timeout>=2.1.0; extra == 'test'
28
+ Requires-Dist: pytest>=8.0.0; extra == 'test'
22
29
  Description-Content-Type: text/markdown
23
30
 
24
31
  <p align="center">
@@ -36,11 +43,6 @@ Description-Content-Type: text/markdown
36
43
  <img src="https://img.shields.io/badge/packaging-uv-D7FF64.svg" alt="Packaging">
37
44
  </p>
38
45
 
39
- > [!IMPORTANT]
40
- > (20.12.25) Из за резкого изменения апи большая часть библиотеки не работает.
41
- Смотрите [новость](https://t.me/pymax_news/111)
42
- >
43
- > P.s добавил логин по qr в dev/1.2.1
44
46
 
45
47
  ---
46
48
  > ⚠️ **Дисклеймер**
@@ -82,6 +84,41 @@ uv add -U maxapi-python
82
84
 
83
85
  ## Быстрый старт
84
86
 
87
+ ### Аутентификация (`device_type`)
88
+
89
+ > [!IMPORTANT]
90
+ > Параметр `device_type` в `UserAgentPayload` **критически важен** для выбора способа авторизации:
91
+
92
+ **Вход по номеру телефона (DESKTOP):**
93
+
94
+ ```python
95
+ from pymax import MaxClient
96
+ from pymax.payloads import UserAgentPayload
97
+
98
+ ua = UserAgentPayload(device_type="DESKTOP", app_version="25.12.13")
99
+
100
+ client = MaxClient(
101
+ phone="+79111111111",
102
+ work_dir="cache",
103
+ headers=ua,
104
+ )
105
+ ```
106
+
107
+ **Вход через QR-код (WEB)** — токен совместим с веб-версией Max:
108
+
109
+ ```python
110
+ from pymax import MaxClient
111
+ from pymax.payloads import UserAgentPayload
112
+
113
+ ua = UserAgentPayload(device_type="WEB", app_version="25.12.13")
114
+
115
+ client = MaxClient(
116
+ phone="+7911111111",
117
+ work_dir="cache",
118
+ headers=ua,
119
+ )
120
+ ```
121
+
85
122
  ### Базовый пример использования
86
123
 
87
124
  ```python
@@ -1,5 +1,5 @@
1
1
  pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
2
- pymax/core.py,sha256=Y4sXaUEtK4pVyYiKf5folyhW_xMFUJ3W-5Hc33b8v0E,14892
2
+ pymax/core.py,sha256=OXGNaQ0pDaf6Ofr1Fb9m7vh5ffpbiMyvUMM0EfwlnIQ,14907
3
3
  pymax/crud.py,sha256=YC92TyhA2mr1tJCcfd-tvh8umtXKgqJfgiLo7nXUl3Q,3076
4
4
  pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
5
5
  pymax/files.py,sha256=AvFIr34Desq2p4CNWXIngRqeyTBKMT98VmcnI-zvUU0,3462
@@ -9,7 +9,7 @@ pymax/formatting.py,sha256=XRtuXJGweuNZevJFdPxksDftIrfuMGEA-AOUc_v6IhQ,2484
9
9
  pymax/interfaces.py,sha256=wKF1z1QRw8LcjvM9rzSHWXTK6gPb6sDt2UGiQLvyMf8,8790
10
10
  pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
11
11
  pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
12
- pymax/payloads.py,sha256=-GEJVXXlmJiFSTX4ToVNzmSZSrvSRe-BLOwYyRxGkWY,7280
12
+ pymax/payloads.py,sha256=GuTLK6HYe_bLW3ardgpKeZ98f79j349tD_6B6EwkGww,7879
13
13
  pymax/types.py,sha256=_ARcVXLGHyiGAJKYPd6EU9QDKzz4VwS6kjTu3YEH_u4,35523
14
14
  pymax/mixins/__init__.py,sha256=5sXJME34S1EssuDETaN4DLRH7vhMw_Q3Jmay9myAIZM,775
15
15
  pymax/mixins/auth.py,sha256=e90vIpEOwAjUxgYMYaG7R6jR_5t9rKsei_mTBQUirL4,14716
@@ -23,10 +23,10 @@ pymax/mixins/socket.py,sha256=tdHgd1NwWoEZhHCDd74XLOHFKUq-rladxhXV8Z_-APU,22860
23
23
  pymax/mixins/telemetry.py,sha256=LWr68DNQkPhAjGRDYQ5lORXxC3Yw6M9E8sF0TCNISTE,3609
24
24
  pymax/mixins/user.py,sha256=RSZd4t-aq8P2k3cVzNVWBkUf-_xTWILrBzwxLRgk1pw,9450
25
25
  pymax/mixins/utils.py,sha256=s3FUf3i_wjn2Gbg5YY1rWZB-90ZEGrrcUuND_MqqSTE,853
26
- pymax/mixins/websocket.py,sha256=m2swhSHIcFG6iABAik_oWxIpHfr0sxZ74I6VRU-iVO8,17809
27
- pymax/static/constant.py,sha256=-qJz65V_ESagy7DYHRj3QsfKwyAyVofWzZh0AqBJtXo,1112
26
+ pymax/mixins/websocket.py,sha256=GpdboEVWzyN1qLTcsgKZym6TlPnklcQuNeXJ5YKwg8c,17724
27
+ pymax/static/constant.py,sha256=nM0svv3VpsVxK-RqoADn9qsTdQvB-IYv0Sgv-bQcWs4,1116
28
28
  pymax/static/enum.py,sha256=Hk0e6zSbGOJC_9Aw7gNXX3hcavnjzQfDyr8vjW22cFo,4648
29
- maxapi_python-1.2.1.dist-info/METADATA,sha256=JU86mxRn8zdfsRGWFnNvJ6ea6KHKmmADCvDWvuvIZb4,5855
30
- maxapi_python-1.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
31
- maxapi_python-1.2.1.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
32
- maxapi_python-1.2.1.dist-info/RECORD,,
29
+ maxapi_python-1.2.2.dist-info/METADATA,sha256=rgiQKdSqYAO743n6jWOy0F76jZyjaGMY7A6qUlHlk64,6753
30
+ maxapi_python-1.2.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
31
+ maxapi_python-1.2.2.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
32
+ maxapi_python-1.2.2.dist-info/RECORD,,
pymax/core.py CHANGED
@@ -295,7 +295,7 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
295
295
  if self._token is None:
296
296
  await self._login()
297
297
 
298
- await self._sync()
298
+ await self._sync(self.user_agent)
299
299
 
300
300
  await self._post_login_tasks(sync=False)
301
301
 
pymax/mixins/websocket.py CHANGED
@@ -50,9 +50,7 @@ class WebSocketMixin(ClientProtocol):
50
50
  payload=payload,
51
51
  ).model_dump(by_alias=True)
52
52
 
53
- self.logger.debug(
54
- "make_message opcode=%s cmd=%s seq=%s", opcode, cmd, self._seq
55
- )
53
+ self.logger.debug("make_message opcode=%s cmd=%s seq=%s", opcode, cmd, self._seq)
56
54
  return msg
57
55
 
58
56
  async def _send_interactive_ping(self) -> None:
@@ -68,9 +66,7 @@ class WebSocketMixin(ClientProtocol):
68
66
  self.logger.warning("Interactive ping failed", exc_info=True)
69
67
  await asyncio.sleep(DEFAULT_PING_INTERVAL)
70
68
 
71
- async def connect(
72
- self, user_agent: UserAgentPayload | None = None
73
- ) -> dict[str, Any] | None:
69
+ async def connect(self, user_agent: UserAgentPayload | None = None) -> dict[str, Any] | None:
74
70
  """
75
71
  Устанавливает соединение WebSocket с сервером и выполняет handshake.
76
72
 
@@ -173,9 +169,7 @@ class WebSocketMixin(ClientProtocol):
173
169
  fut = self._file_upload_waiters.pop(id_, None)
174
170
  if fut and not fut.done():
175
171
  fut.set_result(data)
176
- self.logger.debug(
177
- "Fulfilled file upload waiter for %s=%s", key, id_
178
- )
172
+ self.logger.debug("Fulfilled file upload waiter for %s=%s", key, id_)
179
173
 
180
174
  async def _handle_message_notifications(self, data: dict) -> None:
181
175
  if data.get("opcode") != Opcode.NOTIF_MESSAGE.value:
@@ -359,9 +353,7 @@ class WebSocketMixin(ClientProtocol):
359
353
  )
360
354
  return data
361
355
  except Exception:
362
- self.logger.exception(
363
- "Send and wait failed (opcode=%s, seq=%s)", opcode, msg["seq"]
364
- )
356
+ self.logger.exception("Send and wait failed (opcode=%s, seq=%s)", opcode, msg["seq"])
365
357
  raise RuntimeError("Send and wait failed")
366
358
  finally:
367
359
  self._pending.pop(msg["seq"], None)
@@ -442,7 +434,7 @@ class WebSocketMixin(ClientProtocol):
442
434
  else:
443
435
  return float(2**retry_count)
444
436
 
445
- async def _sync(self) -> None:
437
+ async def _sync(self, user_agent: UserAgentPayload) -> None:
446
438
  self.logger.info("Starting initial sync")
447
439
 
448
440
  payload = SyncPayload(
@@ -453,6 +445,7 @@ class WebSocketMixin(ClientProtocol):
453
445
  presence_sync=0,
454
446
  drafts_sync=0,
455
447
  chats_count=40,
448
+ user_agent=user_agent,
456
449
  ).model_dump(by_alias=True)
457
450
  try:
458
451
  data = await self._send_and_wait(opcode=Opcode.LOGIN, payload=payload)
@@ -473,9 +466,7 @@ class WebSocketMixin(ClientProtocol):
473
466
  self.logger.exception("Error parsing chat entry")
474
467
 
475
468
  if raw_payload.get("profile", {}).get("contact"):
476
- self.me = Me.from_dict(
477
- raw_payload.get("profile", {}).get("contact", {})
478
- )
469
+ self.me = Me.from_dict(raw_payload.get("profile", {}).get("contact", {}))
479
470
 
480
471
  self.logger.info(
481
472
  "Sync completed: dialogs=%d chats=%d channels=%d",
pymax/payloads.py CHANGED
@@ -39,6 +39,20 @@ class BaseWebSocketMessage(BaseModel):
39
39
  payload: dict[str, Any]
40
40
 
41
41
 
42
+ class UserAgentPayload(CamelModel):
43
+ device_type: str = Field(default=DEFAULT_DEVICE_TYPE)
44
+ locale: str = Field(default=DEFAULT_LOCALE)
45
+ device_locale: str = Field(default=DEFAULT_DEVICE_LOCALE)
46
+ os_version: str = Field(default=DEFAULT_OS_VERSION)
47
+ device_name: str = Field(default=DEFAULT_DEVICE_NAME)
48
+ header_user_agent: str = Field(default=DEFAULT_USER_AGENT)
49
+ app_version: str = Field(default=DEFAULT_APP_VERSION)
50
+ screen: str = Field(default=DEFAULT_SCREEN)
51
+ timezone: str = Field(default=DEFAULT_TIMEZONE)
52
+ client_session_id: int = Field(default=DEFAULT_CLIENT_SESSION_ID)
53
+ build_number: int = Field(default=DEFAULT_BUILD_NUMBER)
54
+
55
+
42
56
  class RequestCodePayload(CamelModel):
43
57
  phone: str
44
58
  type: AuthType = AuthType.START_AUTH
@@ -59,6 +73,21 @@ class SyncPayload(CamelModel):
59
73
  presence_sync: int = 0
60
74
  drafts_sync: int = 0
61
75
  chats_count: int = 40
76
+ user_agent: UserAgentPayload = Field(
77
+ default_factory=lambda: UserAgentPayload(
78
+ device_type=DEFAULT_DEVICE_TYPE,
79
+ locale=DEFAULT_LOCALE,
80
+ device_locale=DEFAULT_DEVICE_LOCALE,
81
+ os_version=DEFAULT_OS_VERSION,
82
+ device_name=DEFAULT_DEVICE_NAME,
83
+ header_user_agent=DEFAULT_USER_AGENT,
84
+ app_version=DEFAULT_APP_VERSION,
85
+ screen=DEFAULT_SCREEN,
86
+ timezone=DEFAULT_TIMEZONE,
87
+ client_session_id=DEFAULT_CLIENT_SESSION_ID,
88
+ build_number=DEFAULT_BUILD_NUMBER,
89
+ ),
90
+ )
62
91
 
63
92
 
64
93
  class ReplyLink(CamelModel):
@@ -276,20 +305,6 @@ class RemoveReactionPayload(CamelModel):
276
305
  message_id: str
277
306
 
278
307
 
279
- class UserAgentPayload(CamelModel):
280
- device_type: str = Field(default=DEFAULT_DEVICE_TYPE)
281
- locale: str = Field(default=DEFAULT_LOCALE)
282
- device_locale: str = Field(default=DEFAULT_DEVICE_LOCALE)
283
- os_version: str = Field(default=DEFAULT_OS_VERSION)
284
- device_name: str = Field(default=DEFAULT_DEVICE_NAME)
285
- header_user_agent: str = Field(default=DEFAULT_USER_AGENT)
286
- app_version: str = Field(default=DEFAULT_APP_VERSION)
287
- screen: str = Field(default=DEFAULT_SCREEN)
288
- timezone: str = Field(default=DEFAULT_TIMEZONE)
289
- client_session_id: int = Field(default=DEFAULT_CLIENT_SESSION_ID)
290
- build_number: int = Field(default=DEFAULT_BUILD_NUMBER)
291
-
292
-
293
308
  class ReworkInviteLinkPayload(CamelModel):
294
309
  revoke_private_link: bool = True
295
310
  chat_id: int
pymax/static/constant.py CHANGED
@@ -9,7 +9,7 @@ WEBSOCKET_ORIGIN: Final[Origin] = Origin("https://web.max.ru")
9
9
  HOST: Final[str] = "api.oneme.ru"
10
10
  PORT: Final[int] = 443
11
11
  DEFAULT_TIMEOUT: Final[float] = 20.0
12
- DEFAULT_DEVICE_TYPE: Final[str] = "WEB"
12
+ DEFAULT_DEVICE_TYPE: Final[str] = "DESKTOP"
13
13
  DEFAULT_LOCALE: Final[str] = "ru"
14
14
  DEFAULT_DEVICE_LOCALE: Final[str] = "ru"
15
15
  DEFAULT_DEVICE_NAME: Final[str] = "Chrome"