maxapi-python 1.1.20__py3-none-any.whl → 1.1.21__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.1.21.dist-info}/METADATA +37 -54
- maxapi_python-1.1.21.dist-info/RECORD +32 -0
- pymax/core.py +109 -48
- pymax/crud.py +4 -8
- pymax/filters.py +158 -41
- pymax/formatter.py +1 -0
- pymax/interfaces.py +12 -6
- pymax/mixins/__init__.py +3 -0
- pymax/mixins/auth.py +74 -17
- 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 +243 -257
- 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 +1 -0
- maxapi_python-1.1.20.dist-info/RECORD +0 -31
- {maxapi_python-1.1.20.dist-info → maxapi_python-1.1.21.dist-info}/WHEEL +0 -0
- {maxapi_python-1.1.20.dist-info → maxapi_python-1.1.21.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.1.21
|
|
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
|
|
@@ -79,65 +79,47 @@ uv add -U maxapi-python
|
|
|
79
79
|
|
|
80
80
|
```python
|
|
81
81
|
import asyncio
|
|
82
|
+
|
|
82
83
|
from pymax import MaxClient, Message
|
|
84
|
+
from pymax.filters import Filters
|
|
85
|
+
|
|
86
|
+
client = MaxClient(
|
|
87
|
+
phone="+1234567890",
|
|
88
|
+
work_dir="cache", # директория для сессий
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Обработка входящих сообщений
|
|
93
|
+
@client.on_message(Filters.chat(0)) # фильтр по ID чата
|
|
94
|
+
async def on_message(msg: Message) -> None:
|
|
95
|
+
print(f"[{msg.sender}] {msg.text}")
|
|
83
96
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
await client.send_message(
|
|
98
|
+
chat_id=msg.chat_id,
|
|
99
|
+
text="Привет, я бот на PyMax!",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
await client.add_reaction(
|
|
103
|
+
chat_id=msg.chat_id,
|
|
104
|
+
message_id=str(msg.id),
|
|
105
|
+
reaction="👍",
|
|
106
|
+
)
|
|
87
107
|
|
|
88
|
-
# Обработчик входящих сообщений
|
|
89
|
-
@client.on_message()
|
|
90
|
-
async def handle_message(message: Message) -> None:
|
|
91
|
-
print(f"{message.sender}: {message.text}")
|
|
92
108
|
|
|
93
|
-
# Обработчик запуска клиента
|
|
94
109
|
@client.on_start
|
|
95
|
-
async def
|
|
96
|
-
print("Клиент
|
|
110
|
+
async def on_start() -> None:
|
|
111
|
+
print(f"Клиент запущен. Ваш ID: {client.me.id}")
|
|
97
112
|
|
|
98
|
-
# Получение истории
|
|
113
|
+
# Получение истории
|
|
99
114
|
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()
|
|
115
|
+
print("Последние сообщения из чата 0:")
|
|
116
|
+
for m in history:
|
|
117
|
+
print(f"- {m.text}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def main():
|
|
121
|
+
await client.start() # подключение и авторизация
|
|
122
|
+
|
|
141
123
|
|
|
142
124
|
if __name__ == "__main__":
|
|
143
125
|
asyncio.run(main())
|
|
@@ -145,7 +127,8 @@ if __name__ == "__main__":
|
|
|
145
127
|
|
|
146
128
|
## Документация
|
|
147
129
|
|
|
148
|
-
[
|
|
130
|
+
[GitHub Pages](https://maxapiteam.github.io/PyMax/)
|
|
131
|
+
[DeepWiki](https://deepwiki.com/MaxApiTeam/PyMax)
|
|
149
132
|
|
|
150
133
|
## Лицензия
|
|
151
134
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
|
|
2
|
+
pymax/core.py,sha256=aF-nGQG92KV9ZBIXWTxl2mpyfXgmZ2OQq2-OnouB2O4,19720
|
|
3
|
+
pymax/crud.py,sha256=uphxDTCj1tGCrQ1lE2osLIZY7WLWbS-pkG46i2pU8Z0,3075
|
|
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=hhtmakfcQDzQRsAckPunnJOCKY2lFdLeIp8Yw53yY8s,2522
|
|
9
|
+
pymax/interfaces.py,sha256=2wdS5BfguU9zH3yLSGBWtSq0_SWwcSVLHSd1Z4ZIS2g,4003
|
|
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=zErX2vItVwHV8rAKWXX74dEEYSlECr2oYmnzEzw86eM,9661
|
|
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=9_TtTzB1mXH2U-odtwvOEXViUOCzPXBgTEXbMckhiq4,23122
|
|
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=BjSb2G6CTN6Idpo5GeLa-07YodnLNycYDmI7nOk0YGs,1111
|
|
28
|
+
pymax/static/enum.py,sha256=Lf_qHbA2e2oK7X2uuiXInvaV1ql6hX-4wypFnokaazM,4584
|
|
29
|
+
maxapi_python-1.1.21.dist-info/METADATA,sha256=_VPALryggwzxWHAcYKsKSb4rP094y1PmLEFKIOFolRs,5560
|
|
30
|
+
maxapi_python-1.1.21.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
31
|
+
maxapi_python-1.1.21.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
|
|
32
|
+
maxapi_python-1.1.21.dist-info/RECORD,,
|
pymax/core.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
import contextlib
|
|
3
5
|
import logging
|
|
@@ -8,6 +10,7 @@ import traceback
|
|
|
8
10
|
from collections.abc import Awaitable
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
from typing import TYPE_CHECKING, Any, Literal
|
|
13
|
+
from uuid import UUID
|
|
11
14
|
|
|
12
15
|
from typing_extensions import Self, override
|
|
13
16
|
|
|
@@ -27,12 +30,13 @@ from .static.constant import (
|
|
|
27
30
|
)
|
|
28
31
|
|
|
29
32
|
if TYPE_CHECKING:
|
|
30
|
-
from collections.abc import
|
|
31
|
-
from typing import Any
|
|
33
|
+
from collections.abc import Callable
|
|
32
34
|
|
|
33
35
|
import websockets
|
|
34
36
|
|
|
35
|
-
from .filters import
|
|
37
|
+
from pymax.filters import BaseFilter
|
|
38
|
+
|
|
39
|
+
from .filters import Filters
|
|
36
40
|
from .types import Channel, Chat, Dialog, Me, Message, ReactionInfo, User
|
|
37
41
|
|
|
38
42
|
|
|
@@ -43,29 +47,36 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
43
47
|
"""
|
|
44
48
|
Основной клиент для работы с WebSocket API сервиса Max.
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
50
|
+
:param phone: Номер телефона для авторизации.
|
|
51
|
+
:type phone: str
|
|
52
|
+
:param uri: URI WebSocket сервера.
|
|
53
|
+
:type uri: str, optional
|
|
54
|
+
:param work_dir: Рабочая директория для хранения базы данных.
|
|
55
|
+
:type work_dir: str, optional
|
|
56
|
+
:param logger: Пользовательский логгер. Если не передан, используется логгер модуля с именем f"{__name__}.MaxClient".
|
|
57
|
+
:type logger: logging.Logger | None
|
|
58
|
+
:param headers: Заголовки для подключения к WebSocket.
|
|
59
|
+
:type headers: UserAgentPayload
|
|
60
|
+
:param token: Токен авторизации. Если не передан, будет выполнен процесс логина по номеру телефона.
|
|
61
|
+
:type token: str | None, optional
|
|
62
|
+
:param host: Хост API сервера.
|
|
63
|
+
:type host: str, optional
|
|
64
|
+
:param port: Порт API сервера.
|
|
65
|
+
:type port: int, optional
|
|
66
|
+
:param registration: Флаг регистрации нового пользователя.
|
|
67
|
+
:type registration: bool, optional
|
|
68
|
+
:param first_name: Имя пользователя для регистрации. Требуется, если registration=True.
|
|
69
|
+
:type first_name: str, optional
|
|
70
|
+
:param last_name: Фамилия пользователя для регистрации.
|
|
71
|
+
:type last_name: str | None, optional
|
|
72
|
+
:param send_fake_telemetry: Флаг отправки фейковой телеметрии.
|
|
73
|
+
:type send_fake_telemetry: bool, optional
|
|
74
|
+
:param proxy: Прокси для подключения к WebSocket (см. https://websockets.readthedocs.io/en/stable/topics/proxies.html).
|
|
75
|
+
:type proxy: str | Literal[True] | None, optional
|
|
76
|
+
:param reconnect: Флаг автоматического переподключения при потере соединения.
|
|
77
|
+
:type reconnect: bool, optional
|
|
78
|
+
|
|
79
|
+
:raises InvalidPhoneError: Если формат номера телефона неверный.
|
|
69
80
|
"""
|
|
70
81
|
|
|
71
82
|
def __init__(
|
|
@@ -82,6 +93,7 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
82
93
|
registration: bool = False,
|
|
83
94
|
first_name: str = "",
|
|
84
95
|
last_name: str | None = None,
|
|
96
|
+
device_id: UUID | None = None,
|
|
85
97
|
logger: logging.Logger | None = None,
|
|
86
98
|
reconnect: bool = True,
|
|
87
99
|
reconnect_delay: float = 1.0,
|
|
@@ -127,7 +139,9 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
127
139
|
self._circuit_breaker: bool = False
|
|
128
140
|
self._last_error_time: float = 0.0
|
|
129
141
|
|
|
130
|
-
self._device_id =
|
|
142
|
+
self._device_id = (
|
|
143
|
+
device_id if device_id is not None else self._database.get_device_id()
|
|
144
|
+
)
|
|
131
145
|
self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
132
146
|
|
|
133
147
|
self._token = self._database.get_auth_token() or token
|
|
@@ -138,19 +152,25 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
138
152
|
self._current_screen: str = "chats_list_tab"
|
|
139
153
|
|
|
140
154
|
self._on_message_handlers: list[
|
|
141
|
-
tuple[Callable[[Message], Any],
|
|
155
|
+
tuple[Callable[[Message], Any], BaseFilter[Message] | None]
|
|
142
156
|
] = []
|
|
143
157
|
self._on_message_edit_handlers: list[
|
|
144
|
-
tuple[Callable[[Message], Any],
|
|
158
|
+
tuple[Callable[[Message], Any], BaseFilter[Message] | None]
|
|
145
159
|
] = []
|
|
146
160
|
self._on_message_delete_handlers: list[
|
|
147
|
-
tuple[Callable[[Message], Any],
|
|
161
|
+
tuple[Callable[[Message], Any], BaseFilter[Message] | None]
|
|
148
162
|
] = []
|
|
149
163
|
self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = None
|
|
150
164
|
self._on_reaction_change_handlers: list[
|
|
151
165
|
tuple[Callable[[str, int, ReactionInfo], Any]]
|
|
152
166
|
] = []
|
|
153
167
|
self._on_chat_update_handlers: list[tuple[Callable[[Chat], Any]]] = []
|
|
168
|
+
self._on_raw_receive_handlers: list[
|
|
169
|
+
Callable[[dict[str, Any]], Any | Awaitable[Any]]
|
|
170
|
+
] = []
|
|
171
|
+
self._scheduled_tasks: list[
|
|
172
|
+
tuple[Callable[[], Any | Awaitable[Any]], float]
|
|
173
|
+
] = []
|
|
154
174
|
|
|
155
175
|
self._ssl_context = ssl.create_default_context()
|
|
156
176
|
self._ssl_context.set_ciphers("DEFAULT")
|
|
@@ -199,6 +219,11 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
199
219
|
)
|
|
200
220
|
|
|
201
221
|
async def close(self) -> None:
|
|
222
|
+
"""
|
|
223
|
+
Закрывает клиент и освобождает ресурсы.
|
|
224
|
+
|
|
225
|
+
:return: None
|
|
226
|
+
"""
|
|
202
227
|
try:
|
|
203
228
|
self.logger.info("Closing client")
|
|
204
229
|
if self._recv_task:
|
|
@@ -230,11 +255,11 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
230
255
|
except asyncio.CancelledError:
|
|
231
256
|
raise
|
|
232
257
|
except Exception as e:
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
258
|
+
tb = traceback.format_exc()
|
|
259
|
+
self.logger.error(
|
|
260
|
+
f"Unhandled exception in task {name or coro}: {e}\n{tb}"
|
|
236
261
|
)
|
|
237
|
-
|
|
262
|
+
raise
|
|
238
263
|
|
|
239
264
|
task = asyncio.create_task(runner(), name=name)
|
|
240
265
|
self._background_tasks.add(task)
|
|
@@ -244,21 +269,25 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
244
269
|
if sync:
|
|
245
270
|
await self._sync()
|
|
246
271
|
|
|
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
|
-
|
|
272
|
+
self.logger.debug("is_connected=%s before starting ping", self.is_connected)
|
|
253
273
|
ping_task = asyncio.create_task(self._send_interactive_ping())
|
|
254
274
|
ping_task.add_done_callback(self._log_task_exception)
|
|
255
275
|
self._background_tasks.add(ping_task)
|
|
256
276
|
|
|
277
|
+
start_scheduled_task = asyncio.create_task(self._start_scheduled_tasks())
|
|
278
|
+
start_scheduled_task.add_done_callback(self._log_task_exception)
|
|
279
|
+
|
|
257
280
|
if self._send_fake_telemetry:
|
|
258
281
|
telemetry_task = asyncio.create_task(self._start())
|
|
259
282
|
telemetry_task.add_done_callback(self._log_task_exception)
|
|
260
283
|
self._background_tasks.add(telemetry_task)
|
|
261
284
|
|
|
285
|
+
if self._on_start_handler:
|
|
286
|
+
self.logger.debug("Calling on_start handler")
|
|
287
|
+
result = self._on_start_handler()
|
|
288
|
+
if asyncio.iscoroutine(result):
|
|
289
|
+
await self._safe_execute(result, context="on_start handler")
|
|
290
|
+
|
|
262
291
|
async def _cleanup_client(self) -> None:
|
|
263
292
|
for task in list(self._background_tasks):
|
|
264
293
|
task.cancel()
|
|
@@ -305,17 +334,21 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
305
334
|
"""
|
|
306
335
|
Завершает кастомный login flow: отправляет код, сохраняет токен и запускает пост-логин задачи.
|
|
307
336
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
337
|
+
:param temp_token: Временный токен, полученный из request_code.
|
|
338
|
+
:type temp_token: str
|
|
339
|
+
:param code: Код верификации (6 цифр).
|
|
340
|
+
:type code: str
|
|
341
|
+
:param start: Флаг запуска пост-логин задач и ожидания навсегда. Если False, только сохраняет токен.
|
|
342
|
+
:type start: bool, optional
|
|
343
|
+
:return: None
|
|
344
|
+
:rtype: None
|
|
314
345
|
"""
|
|
315
346
|
resp = await self._send_code(code, temp_token)
|
|
316
347
|
token = resp.get("tokenAttrs", {}).get("LOGIN", {}).get("token")
|
|
348
|
+
if not token:
|
|
349
|
+
raise ValueError("Login response did not contain tokenAttrs.LOGIN.token")
|
|
317
350
|
self._token = token
|
|
318
|
-
self._database.update_auth_token(self._device_id, token)
|
|
351
|
+
self._database.update_auth_token(str(self._device_id), token)
|
|
319
352
|
if start:
|
|
320
353
|
while True:
|
|
321
354
|
try:
|
|
@@ -336,6 +369,9 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
336
369
|
Запускает клиент, подключается к WebSocket, авторизует
|
|
337
370
|
пользователя (если нужно) и запускает фоновый цикл.
|
|
338
371
|
Теперь включает безопасный reconnect-loop, если self.reconnect=True.
|
|
372
|
+
|
|
373
|
+
:return: None
|
|
374
|
+
:rtype: None
|
|
339
375
|
"""
|
|
340
376
|
|
|
341
377
|
while True:
|
|
@@ -349,7 +385,7 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
349
385
|
await self._register(self.first_name, self.last_name)
|
|
350
386
|
|
|
351
387
|
if self._token and self._database.get_auth_token() is None:
|
|
352
|
-
self._database.update_auth_token(self._device_id, self._token)
|
|
388
|
+
self._database.update_auth_token(str(self._device_id), self._token)
|
|
353
389
|
|
|
354
390
|
if self._token is None:
|
|
355
391
|
await self._login()
|
|
@@ -377,8 +413,33 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
377
413
|
await asyncio.sleep(self.reconnect_delay)
|
|
378
414
|
|
|
379
415
|
async def idle(self):
|
|
416
|
+
"""
|
|
417
|
+
Поддерживает клиента в «ожидающем» состоянии до закрытия клиента или иного прерывающего события.
|
|
418
|
+
|
|
419
|
+
:return: Никогда не возвращает значение; функция блокирует выполнение.
|
|
420
|
+
:rtype: None
|
|
421
|
+
"""
|
|
380
422
|
await asyncio.Event().wait()
|
|
381
423
|
|
|
424
|
+
def inspect(self) -> None:
|
|
425
|
+
"""
|
|
426
|
+
Выводит в лог текущий статус клиента для отладки.
|
|
427
|
+
"""
|
|
428
|
+
self.logger.info("Pymax")
|
|
429
|
+
self.logger.info("---------")
|
|
430
|
+
self.logger.info(f"Connected: {self.is_connected}")
|
|
431
|
+
if self.me is not None:
|
|
432
|
+
self.logger.info(f"Me: {self.me.names[0].first_name} ({self.me.id})")
|
|
433
|
+
else:
|
|
434
|
+
self.logger.info("Me: N/A")
|
|
435
|
+
self.logger.info(f"Dialogs: {len(self.dialogs)}")
|
|
436
|
+
self.logger.info(f"Chats: {len(self.chats)}")
|
|
437
|
+
self.logger.info(f"Channels: {len(self.channels)}")
|
|
438
|
+
self.logger.info(f"Users cached: {len(self._users)}")
|
|
439
|
+
self.logger.info(f"Background tasks: {len(self._background_tasks)}")
|
|
440
|
+
self.logger.info(f"Scheduled tasks: {len(self._scheduled_tasks)}")
|
|
441
|
+
self.logger.info("---------")
|
|
442
|
+
|
|
382
443
|
async def __aenter__(self) -> Self:
|
|
383
444
|
self._create_safe_task(self.start(), name="start")
|
|
384
445
|
while not self.is_connected:
|
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)
|
|
@@ -49,11 +48,9 @@ class Database:
|
|
|
49
48
|
session.refresh(auth)
|
|
50
49
|
return auth
|
|
51
50
|
|
|
52
|
-
def update_auth_token(self, device_id:
|
|
51
|
+
def update_auth_token(self, device_id: str, 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()
|
pymax/filters.py
CHANGED
|
@@ -1,47 +1,164 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from pymax.static.enum import AttachType, ChatType, MessageStatus
|
|
8
|
+
from pymax.types import Message
|
|
9
|
+
|
|
10
|
+
T_co = TypeVar("T_co")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseFilter(ABC, Generic[T_co]):
|
|
14
|
+
event_type: type[T_co]
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def __call__(self, event: T_co) -> bool: ...
|
|
18
|
+
|
|
19
|
+
def __and__(self, other: BaseFilter[T_co]) -> BaseFilter[T_co]:
|
|
20
|
+
return AndFilter(self, other)
|
|
21
|
+
|
|
22
|
+
def __or__(self, other: BaseFilter[T_co]) -> BaseFilter[T_co]:
|
|
23
|
+
return OrFilter(self, other)
|
|
24
|
+
|
|
25
|
+
def __invert__(self) -> BaseFilter[T_co]:
|
|
26
|
+
return NotFilter(self)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AndFilter(BaseFilter[T_co]):
|
|
30
|
+
def __init__(self, *filters: BaseFilter[T_co]) -> None:
|
|
31
|
+
self.filters = filters
|
|
32
|
+
self.event_type = filters[0].event_type
|
|
33
|
+
|
|
34
|
+
def __call__(self, event: T_co) -> bool:
|
|
35
|
+
return all(f(event) for f in self.filters)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OrFilter(BaseFilter[T_co]):
|
|
39
|
+
def __init__(self, *filters: BaseFilter[T_co]) -> None:
|
|
40
|
+
self.filters = filters
|
|
41
|
+
self.event_type = filters[0].event_type
|
|
42
|
+
|
|
43
|
+
def __call__(self, event: T_co) -> bool:
|
|
44
|
+
return any(f(event) for f in self.filters)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class NotFilter(BaseFilter[T_co]):
|
|
48
|
+
def __init__(self, base_filter: BaseFilter[T_co]) -> None:
|
|
49
|
+
self.base_filter = base_filter
|
|
50
|
+
self.event_type = base_filter.event_type
|
|
51
|
+
|
|
52
|
+
def __call__(self, event: T_co) -> bool:
|
|
53
|
+
return not self.base_filter(event)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ChatFilter(BaseFilter[Message]):
|
|
57
|
+
event_type = Message
|
|
58
|
+
|
|
59
|
+
def __init__(self, chat_id: int) -> None:
|
|
16
60
|
self.chat_id = chat_id
|
|
17
|
-
|
|
61
|
+
|
|
62
|
+
def __call__(self, message: Message) -> bool:
|
|
63
|
+
return message.chat_id == self.chat_id
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TextFilter(BaseFilter[Message]):
|
|
67
|
+
event_type = Message
|
|
68
|
+
|
|
69
|
+
def __init__(self, text: str) -> None:
|
|
18
70
|
self.text = text
|
|
71
|
+
|
|
72
|
+
def __call__(self, message: Message) -> bool:
|
|
73
|
+
return self.text in message.text
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SenderFilter(BaseFilter[Message]):
|
|
77
|
+
event_type = Message
|
|
78
|
+
|
|
79
|
+
def __init__(self, user_id: int) -> None:
|
|
80
|
+
self.user_id = user_id
|
|
81
|
+
|
|
82
|
+
def __call__(self, message: Message) -> bool:
|
|
83
|
+
return message.sender == self.user_id
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class StatusFilter(BaseFilter[Message]):
|
|
87
|
+
event_type = Message
|
|
88
|
+
|
|
89
|
+
def __init__(self, status: MessageStatus) -> None:
|
|
19
90
|
self.status = status
|
|
20
|
-
self.type = type
|
|
21
|
-
self.reaction_info = reaction_info
|
|
22
|
-
self.text_contains = text_contains
|
|
23
91
|
|
|
24
|
-
def
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
92
|
+
def __call__(self, message: Message) -> bool:
|
|
93
|
+
return message.status == self.status
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TextContainsFilter(BaseFilter[Message]):
|
|
97
|
+
event_type = Message
|
|
98
|
+
|
|
99
|
+
def __init__(self, substring: str) -> None:
|
|
100
|
+
self.substring = substring
|
|
101
|
+
|
|
102
|
+
def __call__(self, message: Message) -> bool:
|
|
103
|
+
return self.substring in message.text
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class RegexTextFilter(BaseFilter[Message]):
|
|
107
|
+
event_type = Message
|
|
108
|
+
|
|
109
|
+
def __init__(self, pattern: str) -> None:
|
|
110
|
+
self.pattern = pattern
|
|
111
|
+
self.regex = re.compile(pattern)
|
|
112
|
+
|
|
113
|
+
def __call__(self, message: Message) -> bool:
|
|
114
|
+
return bool(self.regex.search(message.text))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class MediaFilter(BaseFilter[Message]):
|
|
118
|
+
event_type = Message
|
|
119
|
+
|
|
120
|
+
def __call__(self, message: Message) -> bool:
|
|
121
|
+
return message.attaches is not None and len(message.attaches) > 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class FileFilter(BaseFilter[Message]):
|
|
125
|
+
event_type = Message
|
|
126
|
+
|
|
127
|
+
def __call__(self, message: Message) -> bool:
|
|
128
|
+
if message.attaches is None:
|
|
45
129
|
return False
|
|
130
|
+
return any(attach.type == AttachType.FILE for attach in message.attaches)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Filters:
|
|
134
|
+
@staticmethod
|
|
135
|
+
def chat(chat_id: int) -> BaseFilter[Message]:
|
|
136
|
+
return ChatFilter(chat_id)
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def text(text: str) -> BaseFilter[Message]:
|
|
140
|
+
return TextFilter(text)
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def sender(user_id: int) -> BaseFilter[Message]:
|
|
144
|
+
return SenderFilter(user_id)
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def status(status: MessageStatus) -> BaseFilter[Message]:
|
|
148
|
+
return StatusFilter(status)
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def text_contains(substring: str) -> BaseFilter[Message]:
|
|
152
|
+
return TextContainsFilter(substring)
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def text_matches(pattern: str) -> BaseFilter[Message]:
|
|
156
|
+
return RegexTextFilter(pattern)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def has_media() -> BaseFilter[Message]:
|
|
160
|
+
return MediaFilter()
|
|
46
161
|
|
|
47
|
-
|
|
162
|
+
@staticmethod
|
|
163
|
+
def has_file() -> BaseFilter[Message]:
|
|
164
|
+
return FileFilter()
|