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.
Files changed (168) hide show
  1. maxapi_python-2.0.0.dist-info/METADATA +217 -0
  2. maxapi_python-2.0.0.dist-info/RECORD +140 -0
  3. {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +50 -105
  5. pymax/api/__init__.py +17 -0
  6. pymax/api/auth/__init__.py +1 -0
  7. pymax/api/auth/enums.py +17 -0
  8. pymax/api/auth/payloads.py +129 -0
  9. pymax/api/auth/service.py +313 -0
  10. pymax/api/auth/types.py +13 -0
  11. pymax/api/chats/__init__.py +8 -0
  12. pymax/api/chats/enums.py +27 -0
  13. pymax/api/chats/payloads.py +103 -0
  14. pymax/api/chats/service.py +277 -0
  15. pymax/api/facade.py +32 -0
  16. pymax/api/messages/__init__.py +1 -0
  17. pymax/api/messages/enums.py +17 -0
  18. pymax/api/messages/payloads.py +92 -0
  19. pymax/api/messages/service.py +337 -0
  20. pymax/api/models.py +13 -0
  21. pymax/api/response.py +123 -0
  22. pymax/api/self/__init__.py +2 -0
  23. pymax/api/self/enums.py +11 -0
  24. pymax/api/self/payloads.py +41 -0
  25. pymax/api/self/service.py +142 -0
  26. pymax/api/session/__init__.py +1 -0
  27. pymax/api/session/enums.py +10 -0
  28. pymax/api/session/payloads.py +76 -0
  29. pymax/api/session/service.py +72 -0
  30. pymax/api/uploads/__init__.py +1 -0
  31. pymax/api/uploads/models.py +49 -0
  32. pymax/api/uploads/payloads.py +25 -0
  33. pymax/api/uploads/service.py +458 -0
  34. pymax/api/users/__init__.py +2 -0
  35. pymax/api/users/enums.py +12 -0
  36. pymax/api/users/payloads.py +16 -0
  37. pymax/api/users/service.py +124 -0
  38. pymax/app.py +273 -0
  39. pymax/auth/__init__.py +25 -0
  40. pymax/auth/base.py +37 -0
  41. pymax/auth/email.py +0 -0
  42. pymax/auth/models.py +5 -0
  43. pymax/auth/providers.py +127 -0
  44. pymax/auth/qr.py +135 -0
  45. pymax/auth/service.py +25 -0
  46. pymax/auth/sms.py +122 -0
  47. pymax/base.py +204 -0
  48. pymax/client.py +106 -0
  49. pymax/client_web.py +83 -0
  50. pymax/config.py +215 -0
  51. pymax/connection/__init__.py +1 -0
  52. pymax/connection/connection.py +205 -0
  53. pymax/connection/pending.py +46 -0
  54. pymax/connection/readers/__init__.py +2 -0
  55. pymax/connection/readers/base.py +6 -0
  56. pymax/connection/readers/tcp.py +29 -0
  57. pymax/connection/readers/ws.py +14 -0
  58. pymax/dispatch/__init__.py +10 -0
  59. pymax/dispatch/dispatcher.py +222 -0
  60. pymax/dispatch/enums.py +12 -0
  61. pymax/dispatch/mapping.py +73 -0
  62. pymax/dispatch/resolvers.py +52 -0
  63. pymax/dispatch/router.py +216 -0
  64. pymax/exceptions.py +22 -89
  65. pymax/files/__init__.py +9 -0
  66. pymax/files/base.py +82 -0
  67. pymax/files/file.py +76 -0
  68. pymax/files/photo.py +108 -0
  69. pymax/files/static.py +10 -0
  70. pymax/files/video.py +74 -0
  71. pymax/formatting/__init__.py +0 -0
  72. pymax/formatting/markdown.py +217 -0
  73. pymax/infra/__init__.py +1 -0
  74. pymax/infra/auth.py +55 -0
  75. pymax/infra/base.py +15 -0
  76. pymax/infra/chat.py +240 -0
  77. pymax/infra/message.py +252 -0
  78. pymax/infra/protocol.py +9 -0
  79. pymax/infra/self.py +139 -0
  80. pymax/infra/user.py +107 -0
  81. pymax/logging.py +129 -0
  82. pymax/protocol/__init__.py +11 -0
  83. pymax/protocol/base.py +13 -0
  84. pymax/{static/enum.py → protocol/enums.py} +36 -79
  85. pymax/protocol/models.py +33 -0
  86. pymax/protocol/tcp/__init__.py +1 -0
  87. pymax/protocol/tcp/compression.py +97 -0
  88. pymax/protocol/tcp/framing.py +68 -0
  89. pymax/protocol/tcp/payload.py +127 -0
  90. pymax/protocol/tcp/protocol.py +68 -0
  91. pymax/protocol/ws/__init__.py +1 -0
  92. pymax/protocol/ws/protocol.py +27 -0
  93. pymax/py.typed +0 -0
  94. pymax/routers.py +8 -0
  95. pymax/session/__init__.py +3 -0
  96. pymax/session/models.py +11 -0
  97. pymax/session/protocol.py +14 -0
  98. pymax/session/store.py +232 -0
  99. pymax/telemetry/__init__.py +3 -0
  100. pymax/telemetry/navigation.py +181 -0
  101. pymax/telemetry/payloads.py +142 -0
  102. pymax/telemetry/service.py +225 -0
  103. pymax/transport/__init__.py +0 -0
  104. pymax/transport/base.py +14 -0
  105. pymax/transport/tcp.py +93 -0
  106. pymax/transport/websocket.py +50 -0
  107. pymax/types/__init__.py +2 -0
  108. pymax/types/domain/__init__.py +11 -0
  109. pymax/types/domain/attachments/__init__.py +11 -0
  110. pymax/types/domain/attachments/audio.py +35 -0
  111. pymax/types/domain/attachments/call.py +26 -0
  112. pymax/types/domain/attachments/contact.py +32 -0
  113. pymax/types/domain/attachments/control.py +20 -0
  114. pymax/types/domain/attachments/enums.py +27 -0
  115. pymax/types/domain/attachments/file.py +56 -0
  116. pymax/types/domain/attachments/keyboards/__init__.py +1 -0
  117. pymax/types/domain/attachments/keyboards/inline.py +19 -0
  118. pymax/types/domain/attachments/photo.py +45 -0
  119. pymax/types/domain/attachments/share.py +29 -0
  120. pymax/types/domain/attachments/sticker.py +50 -0
  121. pymax/types/domain/attachments/video.py +90 -0
  122. pymax/types/domain/auth.py +161 -0
  123. pymax/types/domain/base.py +17 -0
  124. pymax/types/domain/chat.py +426 -0
  125. pymax/types/domain/element.py +24 -0
  126. pymax/types/domain/enums.py +24 -0
  127. pymax/types/domain/error.py +20 -0
  128. pymax/types/domain/folder.py +74 -0
  129. pymax/types/domain/login.py +35 -0
  130. pymax/types/domain/message.py +378 -0
  131. pymax/types/domain/name.py +20 -0
  132. pymax/types/domain/profile.py +15 -0
  133. pymax/types/domain/session.py +52 -0
  134. pymax/types/domain/sync.py +80 -0
  135. pymax/types/domain/user.py +117 -0
  136. pymax/types/events/__init__.py +3 -0
  137. pymax/types/events/file.py +5 -0
  138. pymax/types/events/message.py +37 -0
  139. pymax/types/events/video.py +5 -0
  140. maxapi_python-1.2.4.dist-info/METADATA +0 -205
  141. maxapi_python-1.2.4.dist-info/RECORD +0 -33
  142. pymax/core.py +0 -390
  143. pymax/crud.py +0 -96
  144. pymax/files.py +0 -138
  145. pymax/filters.py +0 -164
  146. pymax/formatter.py +0 -31
  147. pymax/formatting.py +0 -74
  148. pymax/interfaces.py +0 -552
  149. pymax/mixins/__init__.py +0 -40
  150. pymax/mixins/auth.py +0 -368
  151. pymax/mixins/channel.py +0 -130
  152. pymax/mixins/group.py +0 -458
  153. pymax/mixins/handler.py +0 -285
  154. pymax/mixins/message.py +0 -879
  155. pymax/mixins/scheduler.py +0 -28
  156. pymax/mixins/self.py +0 -259
  157. pymax/mixins/socket.py +0 -297
  158. pymax/mixins/telemetry.py +0 -112
  159. pymax/mixins/user.py +0 -219
  160. pymax/mixins/websocket.py +0 -142
  161. pymax/models.py +0 -8
  162. pymax/navigation.py +0 -187
  163. pymax/payloads.py +0 -367
  164. pymax/protocols.py +0 -123
  165. pymax/static/constant.py +0 -89
  166. pymax/types.py +0 -1220
  167. pymax/utils.py +0 -90
  168. {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
@@ -0,0 +1,5 @@
1
+ from pymax.types.domain.base import CamelModel
2
+
3
+
4
+ class VideoUploadSignal(CamelModel):
5
+ video_id: int
@@ -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
- [![Star History Chart](https://api.star-history.com/svg?repos=ink-developer/PyMax&type=date&legend=top-left)](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)")