maxapi-python 1.1.20__py3-none-any.whl → 1.2.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.
- {maxapi_python-1.1.20.dist-info → maxapi_python-1.2.1.dist-info}/METADATA +44 -54
- maxapi_python-1.2.1.dist-info/RECORD +32 -0
- pymax/core.py +79 -156
- pymax/crud.py +3 -7
- pymax/filters.py +158 -41
- pymax/formatter.py +1 -0
- pymax/formatting.py +4 -6
- pymax/interfaces.py +148 -8
- pymax/mixins/__init__.py +3 -0
- pymax/mixins/auth.py +229 -30
- pymax/mixins/channel.py +36 -37
- pymax/mixins/group.py +127 -8
- pymax/mixins/handler.py +163 -39
- pymax/mixins/message.py +251 -97
- pymax/mixins/scheduler.py +28 -0
- pymax/mixins/self.py +79 -40
- pymax/mixins/socket.py +254 -281
- pymax/mixins/user.py +63 -42
- pymax/mixins/websocket.py +145 -145
- pymax/payloads.py +12 -0
- pymax/static/constant.py +4 -2
- pymax/static/enum.py +5 -0
- maxapi_python-1.1.20.dist-info/RECORD +0 -31
- {maxapi_python-1.1.20.dist-info → maxapi_python-1.2.1.dist-info}/WHEEL +0 -0
- {maxapi_python-1.1.20.dist-info → maxapi_python-1.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maxapi-python
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
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
|
|
@@ -16,6 +16,7 @@ Requires-Dist: aiofiles>=24.1.0
|
|
|
16
16
|
Requires-Dist: aiohttp>=3.12.15
|
|
17
17
|
Requires-Dist: lz4>=4.4.4
|
|
18
18
|
Requires-Dist: msgpack>=1.1.1
|
|
19
|
+
Requires-Dist: qrcode>=8.2
|
|
19
20
|
Requires-Dist: sqlmodel>=0.0.24
|
|
20
21
|
Requires-Dist: websockets>=15.0
|
|
21
22
|
Description-Content-Type: text/markdown
|
|
@@ -35,6 +36,12 @@ Description-Content-Type: text/markdown
|
|
|
35
36
|
<img src="https://img.shields.io/badge/packaging-uv-D7FF64.svg" alt="Packaging">
|
|
36
37
|
</p>
|
|
37
38
|
|
|
39
|
+
> [!IMPORTANT]
|
|
40
|
+
> (20.12.25) Из за резкого изменения апи большая часть библиотеки не работает.
|
|
41
|
+
Смотрите [новость](https://t.me/pymax_news/111)
|
|
42
|
+
>
|
|
43
|
+
> P.s добавил логин по qr в dev/1.2.1
|
|
44
|
+
|
|
38
45
|
---
|
|
39
46
|
> ⚠️ **Дисклеймер**
|
|
40
47
|
>
|
|
@@ -79,65 +86,47 @@ uv add -U maxapi-python
|
|
|
79
86
|
|
|
80
87
|
```python
|
|
81
88
|
import asyncio
|
|
89
|
+
|
|
82
90
|
from pymax import MaxClient, Message
|
|
91
|
+
from pymax.filters import Filters
|
|
92
|
+
|
|
93
|
+
client = MaxClient(
|
|
94
|
+
phone="+1234567890",
|
|
95
|
+
work_dir="cache", # директория для сессий
|
|
96
|
+
)
|
|
97
|
+
|
|
83
98
|
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
# Обработка входящих сообщений
|
|
100
|
+
@client.on_message(Filters.chat(0)) # фильтр по ID чата
|
|
101
|
+
async def on_message(msg: Message) -> None:
|
|
102
|
+
print(f"[{msg.sender}] {msg.text}")
|
|
103
|
+
|
|
104
|
+
await client.send_message(
|
|
105
|
+
chat_id=msg.chat_id,
|
|
106
|
+
text="Привет, я бот на PyMax!",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
await client.add_reaction(
|
|
110
|
+
chat_id=msg.chat_id,
|
|
111
|
+
message_id=str(msg.id),
|
|
112
|
+
reaction="👍",
|
|
113
|
+
)
|
|
87
114
|
|
|
88
|
-
# Обработчик входящих сообщений
|
|
89
|
-
@client.on_message()
|
|
90
|
-
async def handle_message(message: Message) -> None:
|
|
91
|
-
print(f"{message.sender}: {message.text}")
|
|
92
115
|
|
|
93
|
-
# Обработчик запуска клиента
|
|
94
116
|
@client.on_start
|
|
95
|
-
async def
|
|
96
|
-
print("Клиент
|
|
117
|
+
async def on_start() -> None:
|
|
118
|
+
print(f"Клиент запущен. Ваш ID: {client.me.id}")
|
|
97
119
|
|
|
98
|
-
# Получение истории
|
|
120
|
+
# Получение истории
|
|
99
121
|
history = await client.fetch_history(chat_id=0)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
# Работа с чатами
|
|
110
|
-
for chat in client.chats:
|
|
111
|
-
print(f"Чат: {chat.title}")
|
|
112
|
-
|
|
113
|
-
# Отправка сообщения
|
|
114
|
-
message = await client.send_message(
|
|
115
|
-
"Привет от PyMax!",
|
|
116
|
-
chat.id,
|
|
117
|
-
notify=True
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
# Редактирование сообщения
|
|
121
|
-
await asyncio.sleep(2)
|
|
122
|
-
await client.edit_message(
|
|
123
|
-
chat.id,
|
|
124
|
-
message.id,
|
|
125
|
-
"Привет от PyMax! (отредактировано)"
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
# Удаление сообщения
|
|
129
|
-
await asyncio.sleep(2)
|
|
130
|
-
await client.delete_message(chat.id, [message.id], for_me=False)
|
|
131
|
-
|
|
132
|
-
# Работа с диалогами
|
|
133
|
-
for dialog in client.dialogs:
|
|
134
|
-
print(f"Диалог: {dialog.last_message.text}")
|
|
135
|
-
|
|
136
|
-
# Работа с каналами
|
|
137
|
-
for channel in client.channels:
|
|
138
|
-
print(f"Канал: {channel.title}")
|
|
139
|
-
|
|
140
|
-
await client.close()
|
|
122
|
+
print("Последние сообщения из чата 0:")
|
|
123
|
+
for m in history:
|
|
124
|
+
print(f"- {m.text}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def main():
|
|
128
|
+
await client.start() # подключение и авторизация
|
|
129
|
+
|
|
141
130
|
|
|
142
131
|
if __name__ == "__main__":
|
|
143
132
|
asyncio.run(main())
|
|
@@ -145,7 +134,8 @@ if __name__ == "__main__":
|
|
|
145
134
|
|
|
146
135
|
## Документация
|
|
147
136
|
|
|
148
|
-
[
|
|
137
|
+
[GitHub Pages](https://maxapiteam.github.io/PyMax/)
|
|
138
|
+
[DeepWiki](https://deepwiki.com/MaxApiTeam/PyMax)
|
|
149
139
|
|
|
150
140
|
## Лицензия
|
|
151
141
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
|
|
2
|
+
pymax/core.py,sha256=Y4sXaUEtK4pVyYiKf5folyhW_xMFUJ3W-5Hc33b8v0E,14892
|
|
3
|
+
pymax/crud.py,sha256=YC92TyhA2mr1tJCcfd-tvh8umtXKgqJfgiLo7nXUl3Q,3076
|
|
4
|
+
pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
|
|
5
|
+
pymax/files.py,sha256=AvFIr34Desq2p4CNWXIngRqeyTBKMT98VmcnI-zvUU0,3462
|
|
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=wKF1z1QRw8LcjvM9rzSHWXTK6gPb6sDt2UGiQLvyMf8,8790
|
|
10
|
+
pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
|
|
11
|
+
pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
|
|
12
|
+
pymax/payloads.py,sha256=-GEJVXXlmJiFSTX4ToVNzmSZSrvSRe-BLOwYyRxGkWY,7280
|
|
13
|
+
pymax/types.py,sha256=_ARcVXLGHyiGAJKYPd6EU9QDKzz4VwS6kjTu3YEH_u4,35523
|
|
14
|
+
pymax/mixins/__init__.py,sha256=5sXJME34S1EssuDETaN4DLRH7vhMw_Q3Jmay9myAIZM,775
|
|
15
|
+
pymax/mixins/auth.py,sha256=e90vIpEOwAjUxgYMYaG7R6jR_5t9rKsei_mTBQUirL4,14716
|
|
16
|
+
pymax/mixins/channel.py,sha256=W52YnBay1sUYXxF9oAWsz44ZUh_s45jSvKmAyjTbULM,5357
|
|
17
|
+
pymax/mixins/group.py,sha256=LqI1QHmZlmtuQ0-4H1MrNeBV-O9SMDMfHT9f4B_2poE,15189
|
|
18
|
+
pymax/mixins/handler.py,sha256=ETnI8fA386LYJGjWtUhhWzQHREUA78di1aO1oWwtscA,12523
|
|
19
|
+
pymax/mixins/message.py,sha256=AznKKmTMxdzsYl8IecT43RjWpGvlQM85GzSNGFbI8BA,33279
|
|
20
|
+
pymax/mixins/scheduler.py,sha256=rcMfgfZnzu5V6MkcCg6uRgbi-jkc7UyqOjemulydWbc,964
|
|
21
|
+
pymax/mixins/self.py,sha256=Be5L64eNYylGM-NmoxFpQZv1ohsC1Dx_Cs3Om__V96s,6976
|
|
22
|
+
pymax/mixins/socket.py,sha256=tdHgd1NwWoEZhHCDd74XLOHFKUq-rladxhXV8Z_-APU,22860
|
|
23
|
+
pymax/mixins/telemetry.py,sha256=LWr68DNQkPhAjGRDYQ5lORXxC3Yw6M9E8sF0TCNISTE,3609
|
|
24
|
+
pymax/mixins/user.py,sha256=RSZd4t-aq8P2k3cVzNVWBkUf-_xTWILrBzwxLRgk1pw,9450
|
|
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
|
|
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,,
|
pymax/core.py
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
import contextlib
|
|
3
5
|
import logging
|
|
4
6
|
import socket
|
|
5
7
|
import ssl
|
|
6
8
|
import time
|
|
7
|
-
import traceback
|
|
8
9
|
from collections.abc import Awaitable
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import TYPE_CHECKING, Any, Literal
|
|
12
|
+
from uuid import UUID
|
|
11
13
|
|
|
12
|
-
from typing_extensions import
|
|
14
|
+
from typing_extensions import override
|
|
13
15
|
|
|
14
16
|
from .crud import Database
|
|
15
17
|
from .exceptions import (
|
|
16
18
|
InvalidPhoneError,
|
|
17
19
|
SocketNotConnectedError,
|
|
18
|
-
WebSocketNotConnectedError,
|
|
19
20
|
)
|
|
20
|
-
from .
|
|
21
|
+
from .interfaces import BaseClient
|
|
21
22
|
from .mixins import ApiMixin, SocketMixin, WebSocketMixin
|
|
22
23
|
from .payloads import UserAgentPayload
|
|
23
24
|
from .static.constant import (
|
|
@@ -27,45 +28,53 @@ from .static.constant import (
|
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
if TYPE_CHECKING:
|
|
30
|
-
from collections.abc import
|
|
31
|
-
from typing import Any
|
|
31
|
+
from collections.abc import Callable
|
|
32
32
|
|
|
33
33
|
import websockets
|
|
34
34
|
|
|
35
|
-
from .filters import
|
|
35
|
+
from pymax.filters import BaseFilter
|
|
36
|
+
|
|
37
|
+
from .filters import Filters
|
|
36
38
|
from .types import Channel, Chat, Dialog, Me, Message, ReactionInfo, User
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
logger = logging.getLogger(__name__)
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
class MaxClient(ApiMixin, WebSocketMixin):
|
|
44
|
+
class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
43
45
|
"""
|
|
44
46
|
Основной клиент для работы с WebSocket API сервиса Max.
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
48
|
+
:param phone: Номер телефона для авторизации.
|
|
49
|
+
:type phone: str
|
|
50
|
+
:param uri: URI WebSocket сервера.
|
|
51
|
+
:type uri: str, optional
|
|
52
|
+
:param work_dir: Рабочая директория для хранения базы данных.
|
|
53
|
+
:type work_dir: str, optional
|
|
54
|
+
:param logger: Пользовательский логгер. Если не передан, используется логгер модуля с именем f"{__name__}.MaxClient".
|
|
55
|
+
:type logger: logging.Logger | None
|
|
56
|
+
:param headers: Заголовки для подключения к WebSocket.
|
|
57
|
+
:type headers: UserAgentPayload
|
|
58
|
+
:param token: Токен авторизации. Если не передан, будет выполнен процесс логина по номеру телефона.
|
|
59
|
+
:type token: str | None, optional
|
|
60
|
+
:param host: Хост API сервера.
|
|
61
|
+
:type host: str, optional
|
|
62
|
+
:param port: Порт API сервера.
|
|
63
|
+
:type port: int, optional
|
|
64
|
+
:param registration: Флаг регистрации нового пользователя.
|
|
65
|
+
:type registration: bool, optional
|
|
66
|
+
:param first_name: Имя пользователя для регистрации. Требуется, если registration=True.
|
|
67
|
+
:type first_name: str, optional
|
|
68
|
+
:param last_name: Фамилия пользователя для регистрации.
|
|
69
|
+
:type last_name: str | None, optional
|
|
70
|
+
:param send_fake_telemetry: Флаг отправки фейковой телеметрии.
|
|
71
|
+
:type send_fake_telemetry: bool, optional
|
|
72
|
+
:param proxy: Прокси для подключения к WebSocket (см. https://websockets.readthedocs.io/en/stable/topics/proxies.html).
|
|
73
|
+
:type proxy: str | Literal[True] | None, optional
|
|
74
|
+
:param reconnect: Флаг автоматического переподключения при потере соединения.
|
|
75
|
+
:type reconnect: bool, optional
|
|
76
|
+
|
|
77
|
+
:raises InvalidPhoneError: Если формат номера телефона неверный.
|
|
69
78
|
"""
|
|
70
79
|
|
|
71
80
|
def __init__(
|
|
@@ -82,6 +91,7 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
82
91
|
registration: bool = False,
|
|
83
92
|
first_name: str = "",
|
|
84
93
|
last_name: str | None = None,
|
|
94
|
+
device_id: UUID | None = None,
|
|
85
95
|
logger: logging.Logger | None = None,
|
|
86
96
|
reconnect: bool = True,
|
|
87
97
|
reconnect_delay: float = 1.0,
|
|
@@ -127,7 +137,7 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
127
137
|
self._circuit_breaker: bool = False
|
|
128
138
|
self._last_error_time: float = 0.0
|
|
129
139
|
|
|
130
|
-
self._device_id = self._database.get_device_id()
|
|
140
|
+
self._device_id = device_id if device_id is not None else self._database.get_device_id()
|
|
131
141
|
self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
132
142
|
|
|
133
143
|
self._token = self._database.get_auth_token() or token
|
|
@@ -138,19 +148,20 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
138
148
|
self._current_screen: str = "chats_list_tab"
|
|
139
149
|
|
|
140
150
|
self._on_message_handlers: list[
|
|
141
|
-
tuple[Callable[[Message], Any],
|
|
151
|
+
tuple[Callable[[Message], Any], BaseFilter[Message] | None]
|
|
142
152
|
] = []
|
|
143
153
|
self._on_message_edit_handlers: list[
|
|
144
|
-
tuple[Callable[[Message], Any],
|
|
154
|
+
tuple[Callable[[Message], Any], BaseFilter[Message] | None]
|
|
145
155
|
] = []
|
|
146
156
|
self._on_message_delete_handlers: list[
|
|
147
|
-
tuple[Callable[[Message], Any],
|
|
157
|
+
tuple[Callable[[Message], Any], BaseFilter[Message] | None]
|
|
148
158
|
] = []
|
|
149
159
|
self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = None
|
|
150
|
-
self.
|
|
151
|
-
|
|
152
|
-
] = []
|
|
153
|
-
self.
|
|
160
|
+
self._on_stop_handler: Callable[[], Any | Awaitable[Any]] | None = None
|
|
161
|
+
self._on_reaction_change_handlers: list[Callable[[str, int, ReactionInfo], Any]] = []
|
|
162
|
+
self._on_chat_update_handlers: list[Callable[[Chat], Any | Awaitable[Any]]] = []
|
|
163
|
+
self._on_raw_receive_handlers: list[Callable[[dict[str, Any]], Any | Awaitable[Any]]] = []
|
|
164
|
+
self._scheduled_tasks: list[tuple[Callable[[], Any | Awaitable[Any]], float]] = []
|
|
154
165
|
|
|
155
166
|
self._ssl_context = ssl.create_default_context()
|
|
156
167
|
self._ssl_context.set_ciphers("DEFAULT")
|
|
@@ -168,37 +179,18 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
168
179
|
self._work_dir,
|
|
169
180
|
)
|
|
170
181
|
|
|
171
|
-
def _setup_logger(self) -> None:
|
|
172
|
-
if not self.logger.handlers:
|
|
173
|
-
if not self.logger.level:
|
|
174
|
-
self.logger.setLevel(logging.INFO)
|
|
175
|
-
handler = logging.StreamHandler()
|
|
176
|
-
formatter = ColoredFormatter(
|
|
177
|
-
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
178
|
-
datefmt="%Y-%m-%d %H:%M:%S",
|
|
179
|
-
)
|
|
180
|
-
handler.setFormatter(formatter)
|
|
181
|
-
self.logger.addHandler(handler)
|
|
182
|
-
|
|
183
182
|
async def _wait_forever(self) -> None:
|
|
184
183
|
try:
|
|
185
184
|
await self.ws.wait_closed()
|
|
186
185
|
except asyncio.CancelledError:
|
|
187
186
|
self.logger.debug("wait_closed cancelled")
|
|
188
187
|
|
|
189
|
-
async def
|
|
190
|
-
"""
|
|
191
|
-
Безопасно выполняет пользовательскую корутину.
|
|
192
|
-
Логирует traceback, но не роняет event loop.
|
|
188
|
+
async def close(self) -> None:
|
|
193
189
|
"""
|
|
194
|
-
|
|
195
|
-
return await coro
|
|
196
|
-
except Exception as e:
|
|
197
|
-
self.logger.error(
|
|
198
|
-
f"Unhandled exception in {context}: {e}\n{traceback.format_exc()}"
|
|
199
|
-
)
|
|
190
|
+
Закрывает клиент и освобождает ресурсы.
|
|
200
191
|
|
|
201
|
-
|
|
192
|
+
:return: None
|
|
193
|
+
"""
|
|
202
194
|
try:
|
|
203
195
|
self.logger.info("Closing client")
|
|
204
196
|
if self._recv_task:
|
|
@@ -220,100 +212,46 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
220
212
|
except Exception:
|
|
221
213
|
self.logger.exception("Error closing client")
|
|
222
214
|
|
|
223
|
-
@override
|
|
224
|
-
def _create_safe_task(
|
|
225
|
-
self, coro: Awaitable[Any], *, name: str | None = None
|
|
226
|
-
) -> asyncio.Task[Any | None]:
|
|
227
|
-
async def runner():
|
|
228
|
-
try:
|
|
229
|
-
return await coro
|
|
230
|
-
except asyncio.CancelledError:
|
|
231
|
-
raise
|
|
232
|
-
except Exception as e:
|
|
233
|
-
self.logger.exception(
|
|
234
|
-
f"Unhandled exception in task {name or coro}: {e}",
|
|
235
|
-
exc_info=e,
|
|
236
|
-
)
|
|
237
|
-
return None
|
|
238
|
-
|
|
239
|
-
task = asyncio.create_task(runner(), name=name)
|
|
240
|
-
self._background_tasks.add(task)
|
|
241
|
-
return task
|
|
242
|
-
|
|
243
215
|
async def _post_login_tasks(self, sync: bool = True) -> None:
|
|
244
216
|
if sync:
|
|
245
217
|
await self._sync()
|
|
246
218
|
|
|
247
|
-
|
|
248
|
-
self.logger.debug("Calling on_start handler")
|
|
249
|
-
result = self._on_start_handler()
|
|
250
|
-
if asyncio.iscoroutine(result):
|
|
251
|
-
await self._safe_execute(result, context="on_start handler")
|
|
252
|
-
|
|
219
|
+
self.logger.debug("is_connected=%s before starting ping", self.is_connected)
|
|
253
220
|
ping_task = asyncio.create_task(self._send_interactive_ping())
|
|
254
221
|
ping_task.add_done_callback(self._log_task_exception)
|
|
255
222
|
self._background_tasks.add(ping_task)
|
|
256
223
|
|
|
224
|
+
start_scheduled_task = asyncio.create_task(self._start_scheduled_tasks())
|
|
225
|
+
start_scheduled_task.add_done_callback(self._log_task_exception)
|
|
226
|
+
|
|
257
227
|
if self._send_fake_telemetry:
|
|
258
228
|
telemetry_task = asyncio.create_task(self._start())
|
|
259
229
|
telemetry_task.add_done_callback(self._log_task_exception)
|
|
260
230
|
self._background_tasks.add(telemetry_task)
|
|
261
231
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
await
|
|
267
|
-
except asyncio.CancelledError:
|
|
268
|
-
pass
|
|
269
|
-
except Exception:
|
|
270
|
-
self.logger.debug(
|
|
271
|
-
"Background task raised during cancellation", exc_info=True
|
|
272
|
-
)
|
|
273
|
-
self._background_tasks.discard(task)
|
|
274
|
-
|
|
275
|
-
if self._recv_task:
|
|
276
|
-
self._recv_task.cancel()
|
|
277
|
-
with contextlib.suppress(asyncio.CancelledError):
|
|
278
|
-
await self._recv_task
|
|
279
|
-
self._recv_task = None
|
|
280
|
-
|
|
281
|
-
if self._outgoing_task:
|
|
282
|
-
self._outgoing_task.cancel()
|
|
283
|
-
with contextlib.suppress(asyncio.CancelledError):
|
|
284
|
-
await self._outgoing_task
|
|
285
|
-
self._outgoing_task = None
|
|
286
|
-
|
|
287
|
-
for fut in self._pending.values():
|
|
288
|
-
if not fut.done():
|
|
289
|
-
fut.set_exception(WebSocketNotConnectedError)
|
|
290
|
-
self._pending.clear()
|
|
291
|
-
|
|
292
|
-
if self._ws:
|
|
293
|
-
try:
|
|
294
|
-
await self._ws.close()
|
|
295
|
-
except Exception:
|
|
296
|
-
self.logger.debug("Error closing ws during cleanup", exc_info=True)
|
|
297
|
-
self._ws = None
|
|
298
|
-
|
|
299
|
-
self.is_connected = False
|
|
300
|
-
self.logger.info("Client start() cleaned up")
|
|
232
|
+
if self._on_start_handler:
|
|
233
|
+
self.logger.debug("Calling on_start handler")
|
|
234
|
+
result = self._on_start_handler()
|
|
235
|
+
if asyncio.iscoroutine(result):
|
|
236
|
+
await self._safe_execute(result, context="on_start handler")
|
|
301
237
|
|
|
302
|
-
async def login_with_code(
|
|
303
|
-
self, temp_token: str, code: str, start: bool = False
|
|
304
|
-
) -> None:
|
|
238
|
+
async def login_with_code(self, temp_token: str, code: str, start: bool = False) -> None:
|
|
305
239
|
"""
|
|
306
240
|
Завершает кастомный login flow: отправляет код, сохраняет токен и запускает пост-логин задачи.
|
|
307
241
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
242
|
+
:param temp_token: Временный токен, полученный из request_code.
|
|
243
|
+
:type temp_token: str
|
|
244
|
+
:param code: Код верификации (6 цифр).
|
|
245
|
+
:type code: str
|
|
246
|
+
:param start: Флаг запуска пост-логин задач и ожидания навсегда. Если False, только сохраняет токен.
|
|
247
|
+
:type start: bool, optional
|
|
248
|
+
:return: None
|
|
249
|
+
:rtype: None
|
|
314
250
|
"""
|
|
315
251
|
resp = await self._send_code(code, temp_token)
|
|
316
252
|
token = resp.get("tokenAttrs", {}).get("LOGIN", {}).get("token")
|
|
253
|
+
if not token:
|
|
254
|
+
raise ValueError("Login response did not contain tokenAttrs.LOGIN.token")
|
|
317
255
|
self._token = token
|
|
318
256
|
self._database.update_auth_token(self._device_id, token)
|
|
319
257
|
if start:
|
|
@@ -336,6 +274,9 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
336
274
|
Запускает клиент, подключается к WebSocket, авторизует
|
|
337
275
|
пользователя (если нужно) и запускает фоновый цикл.
|
|
338
276
|
Теперь включает безопасный reconnect-loop, если self.reconnect=True.
|
|
277
|
+
|
|
278
|
+
:return: None
|
|
279
|
+
:rtype: None
|
|
339
280
|
"""
|
|
340
281
|
|
|
341
282
|
while True:
|
|
@@ -353,8 +294,8 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
353
294
|
|
|
354
295
|
if self._token is None:
|
|
355
296
|
await self._login()
|
|
356
|
-
|
|
357
|
-
|
|
297
|
+
|
|
298
|
+
await self._sync()
|
|
358
299
|
|
|
359
300
|
await self._post_login_tasks(sync=False)
|
|
360
301
|
|
|
@@ -376,18 +317,6 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
376
317
|
self.logger.info("Reconnect enabled — restarting client")
|
|
377
318
|
await asyncio.sleep(self.reconnect_delay)
|
|
378
319
|
|
|
379
|
-
async def idle(self):
|
|
380
|
-
await asyncio.Event().wait()
|
|
381
|
-
|
|
382
|
-
async def __aenter__(self) -> Self:
|
|
383
|
-
self._create_safe_task(self.start(), name="start")
|
|
384
|
-
while not self.is_connected:
|
|
385
|
-
await asyncio.sleep(0.05)
|
|
386
|
-
return self
|
|
387
|
-
|
|
388
|
-
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
389
|
-
await self.close()
|
|
390
|
-
|
|
391
320
|
|
|
392
321
|
class SocketMaxClient(SocketMixin, MaxClient):
|
|
393
322
|
@override
|
|
@@ -402,12 +331,6 @@ class SocketMaxClient(SocketMixin, MaxClient):
|
|
|
402
331
|
|
|
403
332
|
@override
|
|
404
333
|
async def _cleanup_client(self):
|
|
405
|
-
"""
|
|
406
|
-
Socket-specific cleanup: cancel background tasks, set pending futures
|
|
407
|
-
exceptions to SocketNotConnectedError, and close socket.
|
|
408
|
-
"""
|
|
409
|
-
from .exceptions import SocketNotConnectedError
|
|
410
|
-
|
|
411
334
|
for task in list(self._background_tasks):
|
|
412
335
|
task.cancel()
|
|
413
336
|
try:
|
pymax/crud.py
CHANGED
|
@@ -31,9 +31,8 @@ class Database:
|
|
|
31
31
|
|
|
32
32
|
def get_device_id(self) -> UUID:
|
|
33
33
|
with self.get_session() as session:
|
|
34
|
-
device_id =
|
|
35
|
-
|
|
36
|
-
)
|
|
34
|
+
device_id = session.exec(select(Auth.device_id)).first()
|
|
35
|
+
|
|
37
36
|
if device_id is None:
|
|
38
37
|
auth = Auth()
|
|
39
38
|
session.add(auth)
|
|
@@ -51,9 +50,7 @@ class Database:
|
|
|
51
50
|
|
|
52
51
|
def update_auth_token(self, device_id: UUID, token: str) -> None:
|
|
53
52
|
with self.get_session() as session:
|
|
54
|
-
auth = session.exec(
|
|
55
|
-
select(Auth).where(Auth.device_id == device_id)
|
|
56
|
-
).first()
|
|
53
|
+
auth = session.exec(select(Auth).where(Auth.device_id == device_id)).first()
|
|
57
54
|
if auth:
|
|
58
55
|
auth.token = token
|
|
59
56
|
session.add(auth)
|
|
@@ -86,7 +83,6 @@ class Database:
|
|
|
86
83
|
with self.get_session() as session:
|
|
87
84
|
rows = session.exec(select(Auth)).all()
|
|
88
85
|
if not rows:
|
|
89
|
-
# Create default Auth with device type from enum
|
|
90
86
|
auth = Auth(device_type=DeviceType.WEB.value)
|
|
91
87
|
session.add(auth)
|
|
92
88
|
session.commit()
|