maxapi-python 1.2.4__py3-none-any.whl → 2.0.0__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-2.0.0.dist-info/METADATA +217 -0
- maxapi_python-2.0.0.dist-info/RECORD +140 -0
- {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/WHEEL +1 -1
- pymax/__init__.py +50 -105
- pymax/api/__init__.py +17 -0
- pymax/api/auth/__init__.py +1 -0
- pymax/api/auth/enums.py +17 -0
- pymax/api/auth/payloads.py +129 -0
- pymax/api/auth/service.py +313 -0
- pymax/api/auth/types.py +13 -0
- pymax/api/chats/__init__.py +8 -0
- pymax/api/chats/enums.py +27 -0
- pymax/api/chats/payloads.py +103 -0
- pymax/api/chats/service.py +277 -0
- pymax/api/facade.py +32 -0
- pymax/api/messages/__init__.py +1 -0
- pymax/api/messages/enums.py +17 -0
- pymax/api/messages/payloads.py +92 -0
- pymax/api/messages/service.py +337 -0
- pymax/api/models.py +13 -0
- pymax/api/response.py +123 -0
- pymax/api/self/__init__.py +2 -0
- pymax/api/self/enums.py +11 -0
- pymax/api/self/payloads.py +41 -0
- pymax/api/self/service.py +142 -0
- pymax/api/session/__init__.py +1 -0
- pymax/api/session/enums.py +10 -0
- pymax/api/session/payloads.py +76 -0
- pymax/api/session/service.py +72 -0
- pymax/api/uploads/__init__.py +1 -0
- pymax/api/uploads/models.py +49 -0
- pymax/api/uploads/payloads.py +25 -0
- pymax/api/uploads/service.py +458 -0
- pymax/api/users/__init__.py +2 -0
- pymax/api/users/enums.py +12 -0
- pymax/api/users/payloads.py +16 -0
- pymax/api/users/service.py +124 -0
- pymax/app.py +273 -0
- pymax/auth/__init__.py +25 -0
- pymax/auth/base.py +37 -0
- pymax/auth/email.py +0 -0
- pymax/auth/models.py +5 -0
- pymax/auth/providers.py +127 -0
- pymax/auth/qr.py +135 -0
- pymax/auth/service.py +25 -0
- pymax/auth/sms.py +122 -0
- pymax/base.py +204 -0
- pymax/client.py +106 -0
- pymax/client_web.py +83 -0
- pymax/config.py +215 -0
- pymax/connection/__init__.py +1 -0
- pymax/connection/connection.py +205 -0
- pymax/connection/pending.py +46 -0
- pymax/connection/readers/__init__.py +2 -0
- pymax/connection/readers/base.py +6 -0
- pymax/connection/readers/tcp.py +29 -0
- pymax/connection/readers/ws.py +14 -0
- pymax/dispatch/__init__.py +10 -0
- pymax/dispatch/dispatcher.py +222 -0
- pymax/dispatch/enums.py +12 -0
- pymax/dispatch/mapping.py +73 -0
- pymax/dispatch/resolvers.py +52 -0
- pymax/dispatch/router.py +216 -0
- pymax/exceptions.py +22 -89
- pymax/files/__init__.py +9 -0
- pymax/files/base.py +82 -0
- pymax/files/file.py +76 -0
- pymax/files/photo.py +108 -0
- pymax/files/static.py +10 -0
- pymax/files/video.py +74 -0
- pymax/formatting/__init__.py +0 -0
- pymax/formatting/markdown.py +217 -0
- pymax/infra/__init__.py +1 -0
- pymax/infra/auth.py +55 -0
- pymax/infra/base.py +15 -0
- pymax/infra/chat.py +240 -0
- pymax/infra/message.py +252 -0
- pymax/infra/protocol.py +9 -0
- pymax/infra/self.py +139 -0
- pymax/infra/user.py +107 -0
- pymax/logging.py +129 -0
- pymax/protocol/__init__.py +11 -0
- pymax/protocol/base.py +13 -0
- pymax/{static/enum.py → protocol/enums.py} +36 -79
- pymax/protocol/models.py +33 -0
- pymax/protocol/tcp/__init__.py +1 -0
- pymax/protocol/tcp/compression.py +97 -0
- pymax/protocol/tcp/framing.py +68 -0
- pymax/protocol/tcp/payload.py +127 -0
- pymax/protocol/tcp/protocol.py +68 -0
- pymax/protocol/ws/__init__.py +1 -0
- pymax/protocol/ws/protocol.py +27 -0
- pymax/py.typed +0 -0
- pymax/routers.py +8 -0
- pymax/session/__init__.py +3 -0
- pymax/session/models.py +11 -0
- pymax/session/protocol.py +14 -0
- pymax/session/store.py +232 -0
- pymax/telemetry/__init__.py +3 -0
- pymax/telemetry/navigation.py +181 -0
- pymax/telemetry/payloads.py +142 -0
- pymax/telemetry/service.py +225 -0
- pymax/transport/__init__.py +0 -0
- pymax/transport/base.py +14 -0
- pymax/transport/tcp.py +93 -0
- pymax/transport/websocket.py +50 -0
- pymax/types/__init__.py +2 -0
- pymax/types/domain/__init__.py +11 -0
- pymax/types/domain/attachments/__init__.py +11 -0
- pymax/types/domain/attachments/audio.py +35 -0
- pymax/types/domain/attachments/call.py +26 -0
- pymax/types/domain/attachments/contact.py +32 -0
- pymax/types/domain/attachments/control.py +20 -0
- pymax/types/domain/attachments/enums.py +27 -0
- pymax/types/domain/attachments/file.py +56 -0
- pymax/types/domain/attachments/keyboards/__init__.py +1 -0
- pymax/types/domain/attachments/keyboards/inline.py +19 -0
- pymax/types/domain/attachments/photo.py +45 -0
- pymax/types/domain/attachments/share.py +29 -0
- pymax/types/domain/attachments/sticker.py +50 -0
- pymax/types/domain/attachments/video.py +90 -0
- pymax/types/domain/auth.py +161 -0
- pymax/types/domain/base.py +17 -0
- pymax/types/domain/chat.py +426 -0
- pymax/types/domain/element.py +24 -0
- pymax/types/domain/enums.py +24 -0
- pymax/types/domain/error.py +20 -0
- pymax/types/domain/folder.py +74 -0
- pymax/types/domain/login.py +35 -0
- pymax/types/domain/message.py +378 -0
- pymax/types/domain/name.py +20 -0
- pymax/types/domain/profile.py +15 -0
- pymax/types/domain/session.py +52 -0
- pymax/types/domain/sync.py +80 -0
- pymax/types/domain/user.py +117 -0
- pymax/types/events/__init__.py +3 -0
- pymax/types/events/file.py +5 -0
- pymax/types/events/message.py +37 -0
- pymax/types/events/video.py +5 -0
- maxapi_python-1.2.4.dist-info/METADATA +0 -205
- maxapi_python-1.2.4.dist-info/RECORD +0 -33
- pymax/core.py +0 -390
- pymax/crud.py +0 -96
- pymax/files.py +0 -138
- pymax/filters.py +0 -164
- pymax/formatter.py +0 -31
- pymax/formatting.py +0 -74
- pymax/interfaces.py +0 -552
- pymax/mixins/__init__.py +0 -40
- pymax/mixins/auth.py +0 -368
- pymax/mixins/channel.py +0 -130
- pymax/mixins/group.py +0 -458
- pymax/mixins/handler.py +0 -285
- pymax/mixins/message.py +0 -879
- pymax/mixins/scheduler.py +0 -28
- pymax/mixins/self.py +0 -259
- pymax/mixins/socket.py +0 -297
- pymax/mixins/telemetry.py +0 -112
- pymax/mixins/user.py +0 -219
- pymax/mixins/websocket.py +0 -142
- pymax/models.py +0 -8
- pymax/navigation.py +0 -187
- pymax/payloads.py +0 -367
- pymax/protocols.py +0 -123
- pymax/static/constant.py +0 -89
- pymax/types.py +0 -1220
- pymax/utils.py +0 -90
- {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from pydantic import PrivateAttr
|
|
6
|
+
|
|
7
|
+
from pymax.types.domain import Chat
|
|
8
|
+
from pymax.types.domain.base import CamelModel
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pymax.api.messages import MessageService
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MessageDeleteEvent(CamelModel):
|
|
15
|
+
"""Событие удаления сообщений.
|
|
16
|
+
|
|
17
|
+
Handler ``on_message_delete`` получает этот объект, когда Max сообщает об
|
|
18
|
+
удалении одного или нескольких сообщений в чате.
|
|
19
|
+
|
|
20
|
+
:ivar chat: Чат, в котором удалены сообщения.
|
|
21
|
+
:vartype chat: Chat
|
|
22
|
+
:ivar message_ids: ID удаленных сообщений.
|
|
23
|
+
:vartype message_ids: list[int]
|
|
24
|
+
:ivar ttl: Признак удаления из-за TTL, если Max его прислал.
|
|
25
|
+
:vartype ttl: bool
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
chat: Chat
|
|
29
|
+
message_ids: list[int]
|
|
30
|
+
ttl: bool = False
|
|
31
|
+
|
|
32
|
+
_actions: MessageService | None = PrivateAttr(default=None)
|
|
33
|
+
|
|
34
|
+
def bind(self, actions: MessageService) -> MessageDeleteEvent:
|
|
35
|
+
"""Привязывает сервис сообщений к событию удаления."""
|
|
36
|
+
self._actions = actions
|
|
37
|
+
return self
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: maxapi-python
|
|
3
|
-
Version: 1.2.4
|
|
4
|
-
Summary: Python wrapper для API мессенджера Max
|
|
5
|
-
Project-URL: Homepage, https://github.com/MaxApiTeam/PyMax
|
|
6
|
-
Project-URL: Repository, https://github.com/MaxApiTeam/PyMax
|
|
7
|
-
Project-URL: Issues, https://github.com/MaxApiTeam/PyMax/issues
|
|
8
|
-
Author-email: ink <mail@gmail.com>
|
|
9
|
-
License-Expression: MIT
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: api,max,messenger,websocket,wrapper
|
|
12
|
-
Classifier: Operating System :: OS Independent
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Requires-Python: >=3.10
|
|
15
|
-
Requires-Dist: aiofiles>=24.1.0
|
|
16
|
-
Requires-Dist: aiohttp>=3.12.15
|
|
17
|
-
Requires-Dist: lz4>=4.4.4
|
|
18
|
-
Requires-Dist: msgpack>=1.1.1
|
|
19
|
-
Requires-Dist: qrcode>=8.2
|
|
20
|
-
Requires-Dist: sqlmodel>=0.0.24
|
|
21
|
-
Requires-Dist: ua-generator>=2.0.19
|
|
22
|
-
Requires-Dist: websockets>=15.0
|
|
23
|
-
Provides-Extra: test
|
|
24
|
-
Requires-Dist: flake8; extra == 'test'
|
|
25
|
-
Requires-Dist: mypy; extra == 'test'
|
|
26
|
-
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'test'
|
|
27
|
-
Requires-Dist: pytest-cov>=5.0.0; extra == 'test'
|
|
28
|
-
Requires-Dist: pytest-timeout>=2.1.0; extra == 'test'
|
|
29
|
-
Requires-Dist: pytest>=8.0.0; extra == 'test'
|
|
30
|
-
Description-Content-Type: text/markdown
|
|
31
|
-
|
|
32
|
-
<p align="center">
|
|
33
|
-
<img src="assets/logo.svg" alt="PyMax" width="400">
|
|
34
|
-
</p>
|
|
35
|
-
|
|
36
|
-
<p align="center">
|
|
37
|
-
<strong>Python wrapper для API мессенджера Max</strong>
|
|
38
|
-
</p>
|
|
39
|
-
|
|
40
|
-
> [!IMPORTANT]
|
|
41
|
-
> (29.12.2025) Снова неожиданное изменение апи, теперь `MaxClient` с `device_type` любым кроме `WEB` не работает, для вохда по номеру телефона используйте `SocketMaxClient`
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<p align="center">
|
|
45
|
-
<img src="https://img.shields.io/badge/python-3.10+-3776AB.svg" alt="Python 3.11+">
|
|
46
|
-
<img src="https://img.shields.io/badge/License-MIT-2f9872.svg" alt="License: MIT">
|
|
47
|
-
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
|
|
48
|
-
<img src="https://img.shields.io/badge/packaging-uv-D7FF64.svg" alt="Packaging">
|
|
49
|
-
</p>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
> ⚠️ **Дисклеймер**
|
|
54
|
-
>
|
|
55
|
-
> * Это **неофициальная** библиотека для работы с внутренним API Max.
|
|
56
|
-
> * Использование может **нарушать условия предоставления услуг** сервиса.
|
|
57
|
-
> * **Вы используете её исключительно на свой страх и риск.**
|
|
58
|
-
> * **Разработчики и контрибьюторы не несут никакой ответственности** за любые последствия использования этого пакета, включая, но не ограничиваясь: блокировку аккаунтов, утерю данных, юридические риски и любые другие проблемы.
|
|
59
|
-
> * API может быть изменен в любой момент без предупреждения.
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## Описание
|
|
63
|
-
|
|
64
|
-
**`pymax`** — асинхронная Python библиотека для работы с API мессенджера Max. Предоставляет интерфейс для отправки сообщений, управления чатами, каналами и диалогами через WebSocket соединение.
|
|
65
|
-
|
|
66
|
-
### Основные возможности
|
|
67
|
-
|
|
68
|
-
- Вход по номеру телефона
|
|
69
|
-
- Отправка, редактирование и удаление сообщений
|
|
70
|
-
- Работа с чатами и каналами
|
|
71
|
-
- История сообщений
|
|
72
|
-
|
|
73
|
-
## Установка
|
|
74
|
-
|
|
75
|
-
> [!IMPORTANT]
|
|
76
|
-
> Для работы библиотеки требуется Python 3.10 или выше
|
|
77
|
-
|
|
78
|
-
### Установка через pip
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
pip install -U maxapi-python
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### Установка через uv
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
uv add -U maxapi-python
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
## Быстрый старт
|
|
91
|
-
|
|
92
|
-
### Аутентификация (`device_type`)
|
|
93
|
-
|
|
94
|
-
> [!IMPORTANT]
|
|
95
|
-
> Параметр `device_type` в `UserAgentPayload` **критически важен** для выбора способа авторизации:
|
|
96
|
-
|
|
97
|
-
**Вход по номеру телефона (DESKTOP):**
|
|
98
|
-
|
|
99
|
-
```python
|
|
100
|
-
from pymax import SocketMaxClient
|
|
101
|
-
from pymax.payloads import UserAgentPayload
|
|
102
|
-
|
|
103
|
-
ua = UserAgentPayload(device_type="DESKTOP", app_version="25.12.13")
|
|
104
|
-
|
|
105
|
-
client = SocketMaxClient(
|
|
106
|
-
phone="+79111111111",
|
|
107
|
-
work_dir="cache",
|
|
108
|
-
headers=ua,
|
|
109
|
-
)
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
**Вход через QR-код (WEB)** — токен совместим с веб-версией Max:
|
|
113
|
-
|
|
114
|
-
```python
|
|
115
|
-
from pymax import MaxClient
|
|
116
|
-
from pymax.payloads import UserAgentPayload
|
|
117
|
-
|
|
118
|
-
ua = UserAgentPayload(device_type="WEB", app_version="25.12.13")
|
|
119
|
-
|
|
120
|
-
client = MaxClient(
|
|
121
|
-
phone="+7911111111",
|
|
122
|
-
work_dir="cache",
|
|
123
|
-
headers=ua,
|
|
124
|
-
)
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### Базовый пример использования
|
|
128
|
-
|
|
129
|
-
```python
|
|
130
|
-
import asyncio
|
|
131
|
-
|
|
132
|
-
from pymax import MaxClient, Message
|
|
133
|
-
from pymax.filters import Filters
|
|
134
|
-
|
|
135
|
-
client = MaxClient(
|
|
136
|
-
phone="+1234567890",
|
|
137
|
-
work_dir="cache", # директория для сессий
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
# Обработка входящих сообщений
|
|
142
|
-
@client.on_message(Filters.chat(0)) # фильтр по ID чата
|
|
143
|
-
async def on_message(msg: Message) -> None:
|
|
144
|
-
print(f"[{msg.sender}] {msg.text}")
|
|
145
|
-
|
|
146
|
-
await client.send_message(
|
|
147
|
-
chat_id=msg.chat_id,
|
|
148
|
-
text="Привет, я бот на PyMax!",
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
await client.add_reaction(
|
|
152
|
-
chat_id=msg.chat_id,
|
|
153
|
-
message_id=str(msg.id),
|
|
154
|
-
reaction="👍",
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
@client.on_start
|
|
159
|
-
async def on_start() -> None:
|
|
160
|
-
print(f"Клиент запущен. Ваш ID: {client.me.id}")
|
|
161
|
-
|
|
162
|
-
# Получение истории
|
|
163
|
-
history = await client.fetch_history(chat_id=0)
|
|
164
|
-
print("Последние сообщения из чата 0:")
|
|
165
|
-
for m in history:
|
|
166
|
-
print(f"- {m.text}")
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
async def main():
|
|
170
|
-
await client.start() # подключение и авторизация
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if __name__ == "__main__":
|
|
174
|
-
asyncio.run(main())
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
## Документация
|
|
178
|
-
|
|
179
|
-
[GitHub Pages](https://maxapiteam.github.io/PyMax/)
|
|
180
|
-
[DeepWiki](https://deepwiki.com/MaxApiTeam/PyMax)
|
|
181
|
-
|
|
182
|
-
## Лицензия
|
|
183
|
-
|
|
184
|
-
Этот проект распространяется под лицензией MIT. См. файл [LICENSE](LICENSE) для получения информации.
|
|
185
|
-
|
|
186
|
-
## Новости
|
|
187
|
-
|
|
188
|
-
[Telegram](https://t.me/pymax_news)
|
|
189
|
-
|
|
190
|
-
## Star History
|
|
191
|
-
|
|
192
|
-
[](https://www.star-history.com/#ink-developer/PyMax&type=date&legend=top-left)
|
|
193
|
-
|
|
194
|
-
## Авторы
|
|
195
|
-
- **[ink](https://github.com/ink-developer)** — Главный разработчик, исследование API и его документация
|
|
196
|
-
- **[noxzion](https://github.com/noxzion)** — Оригинальный автор проекта
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
## Контрибьюторы
|
|
200
|
-
|
|
201
|
-
Спасибо всем за помощь в разработке!
|
|
202
|
-
|
|
203
|
-
<a href="https://github.com/MaxApiTeam/PyMax/graphs/contributors">
|
|
204
|
-
<img src="https://contrib.rocks/image?repo=ink-developer/PyMax" />
|
|
205
|
-
</a>
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
|
|
2
|
-
pymax/core.py,sha256=oJEgLVWHjtTPqDd8I7mdHgYxw-Z567FZx-su-iIhVK8,15904
|
|
3
|
-
pymax/crud.py,sha256=YC92TyhA2mr1tJCcfd-tvh8umtXKgqJfgiLo7nXUl3Q,3076
|
|
4
|
-
pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
|
|
5
|
-
pymax/files.py,sha256=nx7oZfIJ8ZvO-TuG5LzSmk8esbBtNrkKdFQgTQVbUA8,4063
|
|
6
|
-
pymax/filters.py,sha256=gSHPJ1Vi37HKPxf0jRRv9Q3iGwhiQjw1MGrCaouqHzs,4325
|
|
7
|
-
pymax/formatter.py,sha256=RJ_5VbY7Li8UM3xL1AvcXo8v1iYnY8GvDDkreaFqtnY,860
|
|
8
|
-
pymax/formatting.py,sha256=XRtuXJGweuNZevJFdPxksDftIrfuMGEA-AOUc_v6IhQ,2484
|
|
9
|
-
pymax/interfaces.py,sha256=ZFmgZ9sK5j3jG8z7DuVovBkXIvPrN4H4rI0UBb2g4BY,19543
|
|
10
|
-
pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
|
|
11
|
-
pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
|
|
12
|
-
pymax/payloads.py,sha256=m0Fn0eEOo6hrdLB6ACh27ioT3SkYIMDMfLKaPbs2vdo,8147
|
|
13
|
-
pymax/protocols.py,sha256=PoNvri9jFH6WBXGwugrkU6lwtwJEw0DO2s13HOH8_KI,4025
|
|
14
|
-
pymax/types.py,sha256=z1HXNl8CP_X3jTUlENlF9_vzZKdb7gF5PHG5d4rG3BY,37209
|
|
15
|
-
pymax/utils.py,sha256=HK6E6UYyjtUoJ2KXWeDycyiXm_9j5shZme6VFA2ixeM,2960
|
|
16
|
-
pymax/mixins/__init__.py,sha256=5sXJME34S1EssuDETaN4DLRH7vhMw_Q3Jmay9myAIZM,775
|
|
17
|
-
pymax/mixins/auth.py,sha256=nA1fX2ERzk6_WBSrbtBhvwkA5aUXgUdgweytqhZSAJU,14708
|
|
18
|
-
pymax/mixins/channel.py,sha256=Qi5ujw5X7QYx4Lq1XnvlJ3BYjmTFmYDfu7_jRcx4Mx8,5335
|
|
19
|
-
pymax/mixins/group.py,sha256=6bsWSx0ULZonDM_dJSC0EkqpZWd6tv9lmmKWj_gEaaw,15903
|
|
20
|
-
pymax/mixins/handler.py,sha256=duL3Q5Bvv8tjUhOKaDr_k7w09BeCwjVdws2ga7v_zNE,12432
|
|
21
|
-
pymax/mixins/message.py,sha256=MpUED92iWONkJRjb0f7lwPj9gYJhDuv0KKzbveOEaAk,33397
|
|
22
|
-
pymax/mixins/scheduler.py,sha256=K4HB9IfksnXPujJnkipIS5um9nuzC8EjbtQn65RtbfI,963
|
|
23
|
-
pymax/mixins/self.py,sha256=Wn9l3zDF5VzFzzisryOQknf3Ngl81Q98_9jqqbE9ZAw,9174
|
|
24
|
-
pymax/mixins/socket.py,sha256=00uO4-8xer94i6Z6NDTUzzEQbs3NVMH1DB-KoPY6shU,10525
|
|
25
|
-
pymax/mixins/telemetry.py,sha256=EAbGyk8EB6QxijaFQ16vUmFPe6l-gEraCxXnAfhA3kY,3594
|
|
26
|
-
pymax/mixins/user.py,sha256=Xwb2fWM8RCq0SbVhlRyr1RBQGyjlaImtp0lT2PbgEqE,9420
|
|
27
|
-
pymax/mixins/websocket.py,sha256=XZ7lE8rKiNd3MXQRlS7Waz8TuoVWms9ldjCOF12pNTw,4891
|
|
28
|
-
pymax/static/constant.py,sha256=Gu1j4ibpaZL3tI6fUayS_jkyYOYNe9K-QlTUCOGviwQ,2260
|
|
29
|
-
pymax/static/enum.py,sha256=Scyi1pAUaaQlec1YQsU_nvlfxTiQZ6p7gntOJhWfBk4,4773
|
|
30
|
-
maxapi_python-1.2.4.dist-info/METADATA,sha256=hVzJYUCzdYWHmbcvAVlNYrEmvLRoWSVxwSbeNCL0Rqc,7069
|
|
31
|
-
maxapi_python-1.2.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
32
|
-
maxapi_python-1.2.4.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
|
|
33
|
-
maxapi_python-1.2.4.dist-info/RECORD,,
|
pymax/core.py
DELETED
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import contextlib
|
|
5
|
-
import logging
|
|
6
|
-
import socket
|
|
7
|
-
import ssl
|
|
8
|
-
import time
|
|
9
|
-
from collections.abc import Awaitable
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import TYPE_CHECKING, Any, Literal
|
|
12
|
-
from uuid import UUID
|
|
13
|
-
|
|
14
|
-
from typing_extensions import override
|
|
15
|
-
|
|
16
|
-
from .crud import Database
|
|
17
|
-
from .exceptions import (
|
|
18
|
-
InvalidPhoneError,
|
|
19
|
-
SocketNotConnectedError,
|
|
20
|
-
WebSocketNotConnectedError,
|
|
21
|
-
)
|
|
22
|
-
from .interfaces import BaseClient
|
|
23
|
-
from .mixins import ApiMixin, SocketMixin, WebSocketMixin
|
|
24
|
-
from .payloads import UserAgentPayload
|
|
25
|
-
from .static.constant import HOST, PORT, SESSION_STORAGE_DB, WEBSOCKET_URI
|
|
26
|
-
|
|
27
|
-
if TYPE_CHECKING:
|
|
28
|
-
from collections.abc import Callable
|
|
29
|
-
|
|
30
|
-
import websockets
|
|
31
|
-
|
|
32
|
-
from pymax.filters import BaseFilter
|
|
33
|
-
|
|
34
|
-
from .types import Channel, Chat, Dialog, Me, Message, ReactionInfo, User
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
logger = logging.getLogger(__name__)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
41
|
-
allowed_device_types: set[str] = {"WEB"}
|
|
42
|
-
"""
|
|
43
|
-
Основной клиент для работы с WebSocket API сервиса Max.
|
|
44
|
-
|
|
45
|
-
:param phone: Номер телефона для авторизации.
|
|
46
|
-
:type phone: str
|
|
47
|
-
:param uri: URI WebSocket сервера.
|
|
48
|
-
:type uri: str, optional
|
|
49
|
-
:param session_name: Название сессии для хранения базы данных.
|
|
50
|
-
:type session_name: str, optional
|
|
51
|
-
:param work_dir: Рабочая директория для хранения базы данных.
|
|
52
|
-
:type work_dir: str, optional
|
|
53
|
-
:param logger: Пользовательский логгер. Если не передан, используется логгер модуля с именем f"{__name__}.MaxClient".
|
|
54
|
-
:type logger: logging.Logger | None
|
|
55
|
-
:param headers: Заголовки для подключения к WebSocket.
|
|
56
|
-
:type headers: UserAgentPayload
|
|
57
|
-
:param token: Токен авторизации. Если не передан, будет выполнен процесс логина по номеру телефона.
|
|
58
|
-
:type token: str | None, optional
|
|
59
|
-
:param host: Хост API сервера.
|
|
60
|
-
:type host: str, optional
|
|
61
|
-
:param port: Порт API сервера.
|
|
62
|
-
:type port: int, optional
|
|
63
|
-
:param registration: Флаг регистрации нового пользователя.
|
|
64
|
-
:type registration: bool, optional
|
|
65
|
-
:param first_name: Имя пользователя для регистрации. Требуется, если registration=True.
|
|
66
|
-
:type first_name: str, optional
|
|
67
|
-
:param last_name: Фамилия пользователя для регистрации.
|
|
68
|
-
:type last_name: str | None, optional
|
|
69
|
-
:param send_fake_telemetry: Флаг отправки фейковой телеметрии.
|
|
70
|
-
:type send_fake_telemetry: bool, optional
|
|
71
|
-
:param proxy: Прокси для подключения к WebSocket (см. https://websockets.readthedocs.io/en/stable/topics/proxies.html).
|
|
72
|
-
:type proxy: str | Literal[True] | None, optional
|
|
73
|
-
:param reconnect: Флаг автоматического переподключения при потере соединения.
|
|
74
|
-
:type reconnect: bool, optional
|
|
75
|
-
|
|
76
|
-
:raises InvalidPhoneError: Если формат номера телефона неверный.
|
|
77
|
-
"""
|
|
78
|
-
|
|
79
|
-
def __init__(
|
|
80
|
-
self,
|
|
81
|
-
phone: str,
|
|
82
|
-
uri: str = WEBSOCKET_URI,
|
|
83
|
-
session_name: str = SESSION_STORAGE_DB,
|
|
84
|
-
headers: UserAgentPayload | None = None,
|
|
85
|
-
token: str | None = None,
|
|
86
|
-
send_fake_telemetry: bool = True,
|
|
87
|
-
host: str = HOST,
|
|
88
|
-
port: int = PORT,
|
|
89
|
-
proxy: str | Literal[True] | None = None,
|
|
90
|
-
work_dir: str = ".",
|
|
91
|
-
registration: bool = False,
|
|
92
|
-
first_name: str = "",
|
|
93
|
-
last_name: str | None = None,
|
|
94
|
-
device_id: UUID | None = None,
|
|
95
|
-
logger: logging.Logger | None = None,
|
|
96
|
-
reconnect: bool = True,
|
|
97
|
-
reconnect_delay: float = 1.0,
|
|
98
|
-
) -> None:
|
|
99
|
-
self.logger = logger or logging.getLogger(f"{__name__}")
|
|
100
|
-
self.uri: str = uri
|
|
101
|
-
self.phone: str = phone
|
|
102
|
-
if not self._check_phone():
|
|
103
|
-
raise InvalidPhoneError(self.phone)
|
|
104
|
-
self.host: str = host
|
|
105
|
-
self.port: int = port
|
|
106
|
-
self.registration: bool = registration
|
|
107
|
-
self.first_name: str = first_name
|
|
108
|
-
self.last_name: str | None = last_name
|
|
109
|
-
self.proxy: str | Literal[True] | None = proxy
|
|
110
|
-
self.reconnect: bool = reconnect
|
|
111
|
-
self.reconnect_delay: float = reconnect_delay
|
|
112
|
-
|
|
113
|
-
self.is_connected: bool = False
|
|
114
|
-
|
|
115
|
-
self.chats: list[Chat] = []
|
|
116
|
-
self.dialogs: list[Dialog] = []
|
|
117
|
-
self.channels: list[Channel] = []
|
|
118
|
-
self.me: Me | None = None
|
|
119
|
-
self.contacts: list[User] = []
|
|
120
|
-
self._users: dict[int, User] = {}
|
|
121
|
-
|
|
122
|
-
self._work_dir: str = work_dir
|
|
123
|
-
self._database_path: Path = Path(work_dir) / session_name
|
|
124
|
-
self._database_path.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
-
self._database_path.touch(exist_ok=True)
|
|
126
|
-
self._database = Database(self._work_dir)
|
|
127
|
-
|
|
128
|
-
self._incoming: asyncio.Queue[dict[str, Any]] | None = None
|
|
129
|
-
self._outgoing: asyncio.Queue[dict[str, Any]] | None = None
|
|
130
|
-
self._recv_task: asyncio.Task[Any] | None = None
|
|
131
|
-
self._outgoing_task: asyncio.Task[Any] | None = None
|
|
132
|
-
self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
133
|
-
self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
134
|
-
self._background_tasks: set[asyncio.Task[Any]] = set()
|
|
135
|
-
self._stop_event = asyncio.Event()
|
|
136
|
-
|
|
137
|
-
self._seq: int = 0
|
|
138
|
-
self._error_count: int = 0
|
|
139
|
-
self._circuit_breaker: bool = False
|
|
140
|
-
self._last_error_time: float = 0.0
|
|
141
|
-
|
|
142
|
-
self._device_id = device_id if device_id is not None else self._database.get_device_id()
|
|
143
|
-
self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
144
|
-
|
|
145
|
-
self._token = self._database.get_auth_token() or token
|
|
146
|
-
if headers is None:
|
|
147
|
-
headers = self._default_headers()
|
|
148
|
-
self.user_agent = headers
|
|
149
|
-
self._validate_device_type()
|
|
150
|
-
self._send_fake_telemetry: bool = send_fake_telemetry
|
|
151
|
-
self._session_id: int = int(time.time() * 1000)
|
|
152
|
-
self._action_id: int = 1
|
|
153
|
-
self._current_screen: str = "chats_list_tab"
|
|
154
|
-
|
|
155
|
-
self._on_message_handlers: list[
|
|
156
|
-
tuple[Callable[[Message], Any], BaseFilter[Message] | None]
|
|
157
|
-
] = []
|
|
158
|
-
self._on_message_edit_handlers: list[
|
|
159
|
-
tuple[Callable[[Message], Any], BaseFilter[Message] | None]
|
|
160
|
-
] = []
|
|
161
|
-
self._on_message_delete_handlers: list[
|
|
162
|
-
tuple[Callable[[Message], Any], BaseFilter[Message] | None]
|
|
163
|
-
] = []
|
|
164
|
-
self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = None
|
|
165
|
-
self._on_stop_handler: Callable[[], Any | Awaitable[Any]] | None = None
|
|
166
|
-
self._on_reaction_change_handlers: list[Callable[[str, int, ReactionInfo], Any]] = []
|
|
167
|
-
self._on_chat_update_handlers: list[Callable[[Chat], Any | Awaitable[Any]]] = []
|
|
168
|
-
self._on_raw_receive_handlers: list[Callable[[dict[str, Any]], Any | Awaitable[Any]]] = []
|
|
169
|
-
self._scheduled_tasks: list[tuple[Callable[[], Any | Awaitable[Any]], float]] = []
|
|
170
|
-
|
|
171
|
-
self._ssl_context = ssl.create_default_context()
|
|
172
|
-
self._ssl_context.set_ciphers("DEFAULT")
|
|
173
|
-
self._ssl_context.check_hostname = True
|
|
174
|
-
self._ssl_context.verify_mode = ssl.CERT_REQUIRED
|
|
175
|
-
self._ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
176
|
-
self._ssl_context.load_default_certs()
|
|
177
|
-
self._socket: socket.socket | None = None
|
|
178
|
-
self._ws: websockets.ClientConnection | None = None
|
|
179
|
-
|
|
180
|
-
self._setup_logger()
|
|
181
|
-
self.logger.debug(
|
|
182
|
-
"Initialized MaxClient uri=%s work_dir=%s",
|
|
183
|
-
self.uri,
|
|
184
|
-
self._work_dir,
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
@staticmethod
|
|
188
|
-
def _default_headers() -> UserAgentPayload:
|
|
189
|
-
return UserAgentPayload(device_type="WEB")
|
|
190
|
-
|
|
191
|
-
def _validate_device_type(self) -> None:
|
|
192
|
-
if self.user_agent.device_type not in self.allowed_device_types:
|
|
193
|
-
raise ValueError(
|
|
194
|
-
f"{self.__class__.__name__} does not support "
|
|
195
|
-
f"device_type={self.user_agent.device_type}"
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
async def _wait_forever(self) -> None:
|
|
199
|
-
try:
|
|
200
|
-
await self.ws.wait_closed()
|
|
201
|
-
except asyncio.CancelledError:
|
|
202
|
-
self.logger.debug("wait_closed cancelled")
|
|
203
|
-
except WebSocketNotConnectedError:
|
|
204
|
-
self.logger.info("WebSocket not connected, exiting wait_forever")
|
|
205
|
-
|
|
206
|
-
async def close(self) -> None:
|
|
207
|
-
"""
|
|
208
|
-
Закрывает клиент и освобождает ресурсы.
|
|
209
|
-
|
|
210
|
-
:return: None
|
|
211
|
-
"""
|
|
212
|
-
try:
|
|
213
|
-
self.logger.info("Closing client")
|
|
214
|
-
self._stop_event.set()
|
|
215
|
-
except Exception:
|
|
216
|
-
self.logger.exception("Error closing client")
|
|
217
|
-
|
|
218
|
-
async def _post_login_tasks(self, sync: bool = True) -> None:
|
|
219
|
-
if sync:
|
|
220
|
-
await self._sync()
|
|
221
|
-
|
|
222
|
-
self.logger.debug("is_connected=%s before starting ping", self.is_connected)
|
|
223
|
-
ping_task = asyncio.create_task(self._send_interactive_ping())
|
|
224
|
-
ping_task.add_done_callback(self._log_task_exception)
|
|
225
|
-
self._background_tasks.add(ping_task)
|
|
226
|
-
|
|
227
|
-
start_scheduled_task = asyncio.create_task(self._start_scheduled_tasks())
|
|
228
|
-
start_scheduled_task.add_done_callback(self._log_task_exception)
|
|
229
|
-
|
|
230
|
-
if self._send_fake_telemetry:
|
|
231
|
-
telemetry_task = asyncio.create_task(self._start())
|
|
232
|
-
telemetry_task.add_done_callback(self._log_task_exception)
|
|
233
|
-
self._background_tasks.add(telemetry_task)
|
|
234
|
-
|
|
235
|
-
if self._on_start_handler:
|
|
236
|
-
self.logger.debug("Calling on_start handler")
|
|
237
|
-
result = self._on_start_handler()
|
|
238
|
-
if asyncio.iscoroutine(result):
|
|
239
|
-
await self._safe_execute(result, context="on_start handler")
|
|
240
|
-
|
|
241
|
-
async def login_with_code(self, temp_token: str, code: str, start: bool = False) -> None:
|
|
242
|
-
"""
|
|
243
|
-
Завершает кастомный login flow: отправляет код, сохраняет токен и запускает пост-логин задачи.
|
|
244
|
-
|
|
245
|
-
:param temp_token: Временный токен, полученный из request_code.
|
|
246
|
-
:type temp_token: str
|
|
247
|
-
:param code: Код верификации (6 цифр).
|
|
248
|
-
:type code: str
|
|
249
|
-
:param start: Флаг запуска пост-логин задач и ожидания навсегда. Если False, только сохраняет токен.
|
|
250
|
-
:type start: bool, optional
|
|
251
|
-
:return: None
|
|
252
|
-
:rtype: None
|
|
253
|
-
"""
|
|
254
|
-
resp = await self._send_code(code, temp_token)
|
|
255
|
-
token = resp.get("tokenAttrs", {}).get("LOGIN", {}).get("token")
|
|
256
|
-
if not token:
|
|
257
|
-
raise ValueError("Login response did not contain tokenAttrs.LOGIN.token")
|
|
258
|
-
self._token = token
|
|
259
|
-
self._database.update_auth_token(self._device_id, token)
|
|
260
|
-
if start:
|
|
261
|
-
while True:
|
|
262
|
-
try:
|
|
263
|
-
await self._post_login_tasks()
|
|
264
|
-
await self._wait_forever()
|
|
265
|
-
except Exception:
|
|
266
|
-
self.logger.exception("Error during post-login tasks")
|
|
267
|
-
finally:
|
|
268
|
-
await self._cleanup_client()
|
|
269
|
-
|
|
270
|
-
self.logger.info("Reconnecting after post-login tasks failure")
|
|
271
|
-
await asyncio.sleep(self.reconnect_delay)
|
|
272
|
-
else:
|
|
273
|
-
self.logger.info("Login successful, token saved to database, exiting...")
|
|
274
|
-
|
|
275
|
-
async def start(self) -> None:
|
|
276
|
-
"""
|
|
277
|
-
Запускает клиент, подключается к WebSocket, авторизует
|
|
278
|
-
пользователя (если нужно) и запускает фоновый цикл.
|
|
279
|
-
Теперь включает безопасный reconnect-loop, если self.reconnect=True.
|
|
280
|
-
|
|
281
|
-
:return: None
|
|
282
|
-
:rtype: None
|
|
283
|
-
"""
|
|
284
|
-
self.logger.info("Client starting")
|
|
285
|
-
while not self._stop_event.is_set():
|
|
286
|
-
try:
|
|
287
|
-
await self.connect(self.user_agent)
|
|
288
|
-
|
|
289
|
-
if self.registration:
|
|
290
|
-
if not self.first_name:
|
|
291
|
-
raise ValueError("First name is required for registration")
|
|
292
|
-
await self._register(self.first_name, self.last_name)
|
|
293
|
-
|
|
294
|
-
if self._token and self._database.get_auth_token() is None:
|
|
295
|
-
self._database.update_auth_token(self._device_id, self._token)
|
|
296
|
-
|
|
297
|
-
if self._token is None:
|
|
298
|
-
await self._login()
|
|
299
|
-
|
|
300
|
-
await self._sync(self.user_agent)
|
|
301
|
-
await self._post_login_tasks(sync=False)
|
|
302
|
-
|
|
303
|
-
wait_task = asyncio.create_task(self._wait_forever())
|
|
304
|
-
stop_task = asyncio.create_task(self._stop_event.wait())
|
|
305
|
-
|
|
306
|
-
done, pending = await asyncio.wait(
|
|
307
|
-
[wait_task, stop_task], return_when=asyncio.FIRST_COMPLETED
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
for task in pending:
|
|
311
|
-
task.cancel()
|
|
312
|
-
with contextlib.suppress(asyncio.CancelledError):
|
|
313
|
-
await task
|
|
314
|
-
|
|
315
|
-
except asyncio.CancelledError:
|
|
316
|
-
self.logger.info("Client task cancelled, stopping")
|
|
317
|
-
break
|
|
318
|
-
except Exception as e:
|
|
319
|
-
self.logger.exception("Client start iteration failed")
|
|
320
|
-
finally:
|
|
321
|
-
await self._cleanup_client()
|
|
322
|
-
|
|
323
|
-
if not self.reconnect or self._stop_event.is_set():
|
|
324
|
-
self.logger.info("Reconnect disabled or stop requested — exiting start()")
|
|
325
|
-
break
|
|
326
|
-
|
|
327
|
-
self.logger.info("Reconnect enabled — restarting client")
|
|
328
|
-
await asyncio.sleep(self.reconnect_delay)
|
|
329
|
-
|
|
330
|
-
self.logger.info("Client exited cleanly")
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
class SocketMaxClient(SocketMixin, MaxClient):
|
|
334
|
-
allowed_device_types = {"ANDROID", "IOS", "DESKTOP"}
|
|
335
|
-
|
|
336
|
-
@staticmethod
|
|
337
|
-
def _default_headers() -> UserAgentPayload:
|
|
338
|
-
return UserAgentPayload(device_type="DESKTOP")
|
|
339
|
-
|
|
340
|
-
@override
|
|
341
|
-
async def _wait_forever(self):
|
|
342
|
-
if self._recv_task:
|
|
343
|
-
try:
|
|
344
|
-
await self._recv_task
|
|
345
|
-
except asyncio.CancelledError:
|
|
346
|
-
self.logger.debug("Socket recv_task cancelled")
|
|
347
|
-
except Exception as e:
|
|
348
|
-
self.logger.exception("Socket recv_task failed: %s", e)
|
|
349
|
-
|
|
350
|
-
@override
|
|
351
|
-
async def _cleanup_client(self):
|
|
352
|
-
for task in list(self._background_tasks):
|
|
353
|
-
task.cancel()
|
|
354
|
-
try:
|
|
355
|
-
await task
|
|
356
|
-
except asyncio.CancelledError:
|
|
357
|
-
pass
|
|
358
|
-
except Exception:
|
|
359
|
-
self.logger.debug(
|
|
360
|
-
"Background task raised during cancellation (socket)",
|
|
361
|
-
exc_info=True,
|
|
362
|
-
)
|
|
363
|
-
self._background_tasks.discard(task)
|
|
364
|
-
|
|
365
|
-
if self._recv_task:
|
|
366
|
-
self._recv_task.cancel()
|
|
367
|
-
with contextlib.suppress(asyncio.CancelledError):
|
|
368
|
-
await self._recv_task
|
|
369
|
-
self._recv_task = None
|
|
370
|
-
|
|
371
|
-
if self._outgoing_task:
|
|
372
|
-
self._outgoing_task.cancel()
|
|
373
|
-
with contextlib.suppress(asyncio.CancelledError):
|
|
374
|
-
await self._outgoing_task
|
|
375
|
-
self._outgoing_task = None
|
|
376
|
-
|
|
377
|
-
for fut in self._pending.values():
|
|
378
|
-
if not fut.done():
|
|
379
|
-
fut.set_exception(SocketNotConnectedError())
|
|
380
|
-
self._pending.clear()
|
|
381
|
-
|
|
382
|
-
if self._socket:
|
|
383
|
-
try:
|
|
384
|
-
self._socket.close()
|
|
385
|
-
except Exception:
|
|
386
|
-
self.logger.debug("Error closing socket during cleanup", exc_info=True)
|
|
387
|
-
self._socket = None
|
|
388
|
-
|
|
389
|
-
self.is_connected = False
|
|
390
|
-
self.logger.info("Client start() cleaned up (socket)")
|