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
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()
|
pymax/formatter.py
CHANGED
pymax/formatting.py
CHANGED
|
@@ -46,14 +46,12 @@ class Formatting:
|
|
|
46
46
|
|
|
47
47
|
if inner_text is not None and fmt_type is not None:
|
|
48
48
|
next_pos = match.end()
|
|
49
|
-
has_newline = (
|
|
50
|
-
next_pos
|
|
51
|
-
)
|
|
49
|
+
has_newline = (next_pos < len(text) and text[next_pos] == "\n") or (
|
|
50
|
+
next_pos == len(text)
|
|
51
|
+
)
|
|
52
52
|
|
|
53
53
|
length = len(inner_text) + (1 if has_newline else 0)
|
|
54
|
-
elements.append(
|
|
55
|
-
Element(type=fmt_type, from_=current_pos, length=length)
|
|
56
|
-
)
|
|
54
|
+
elements.append(Element(type=fmt_type, from_=current_pos, length=length))
|
|
57
55
|
|
|
58
56
|
clean_parts.append(inner_text)
|
|
59
57
|
if has_newline:
|
pymax/interfaces.py
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import logging
|
|
2
4
|
import socket
|
|
3
5
|
import ssl
|
|
6
|
+
import traceback
|
|
4
7
|
from abc import ABC, abstractmethod
|
|
5
8
|
from collections.abc import Awaitable, Callable
|
|
6
9
|
from logging import Logger
|
|
7
10
|
from typing import TYPE_CHECKING, Any, Literal
|
|
8
11
|
|
|
12
|
+
from typing_extensions import Self
|
|
13
|
+
|
|
14
|
+
from pymax.exceptions import WebSocketNotConnectedError
|
|
15
|
+
from pymax.formatter import ColoredFormatter
|
|
16
|
+
|
|
9
17
|
from .payloads import UserAgentPayload
|
|
10
18
|
from .static.constant import DEFAULT_TIMEOUT
|
|
11
19
|
from .static.enum import Opcode
|
|
@@ -21,7 +29,7 @@ if TYPE_CHECKING:
|
|
|
21
29
|
from pymax.types import ReactionInfo
|
|
22
30
|
|
|
23
31
|
from .crud import Database
|
|
24
|
-
from .filters import
|
|
32
|
+
from .filters import BaseFilter
|
|
25
33
|
|
|
26
34
|
|
|
27
35
|
class ClientProtocol(ABC):
|
|
@@ -67,18 +75,18 @@ class ClientProtocol(ABC):
|
|
|
67
75
|
self._action_id: int = 0
|
|
68
76
|
self._current_screen: str = "chats_list_tab"
|
|
69
77
|
self._on_message_handlers: list[
|
|
70
|
-
tuple[Callable[[Message], Any],
|
|
78
|
+
tuple[Callable[[Message], Any], BaseFilter[Message] | None]
|
|
71
79
|
] = []
|
|
72
80
|
self._on_message_edit_handlers: list[
|
|
73
|
-
tuple[Callable[[Message], Any],
|
|
81
|
+
tuple[Callable[[Message], Any], BaseFilter[Message] | None]
|
|
74
82
|
] = []
|
|
75
83
|
self._on_message_delete_handlers: list[
|
|
76
|
-
tuple[Callable[[Message], Any],
|
|
77
|
-
] = []
|
|
78
|
-
self._on_reaction_change_handlers: list[
|
|
79
|
-
tuple[Callable[[str, int, ReactionInfo], Any]]
|
|
84
|
+
tuple[Callable[[Message], Any], BaseFilter[Message] | None]
|
|
80
85
|
] = []
|
|
81
|
-
self.
|
|
86
|
+
self._on_reaction_change_handlers: list[Callable[[str, int, ReactionInfo], Any]] = []
|
|
87
|
+
self._on_chat_update_handlers: list[Callable[[Chat], Any | Awaitable[Any]]] = []
|
|
88
|
+
self._on_raw_receive_handlers: list[Callable[[dict[str, Any]], Any | Awaitable[Any]]] = []
|
|
89
|
+
self._scheduled_tasks: list[tuple[Callable[[], Any | Awaitable[Any]], float]] = []
|
|
82
90
|
self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = None
|
|
83
91
|
self._background_tasks: set[asyncio.Task[Any]] = set()
|
|
84
92
|
self._ssl_context: ssl.SSLContext
|
|
@@ -114,3 +122,135 @@ class ClientProtocol(ABC):
|
|
|
114
122
|
self, coro: Awaitable[Any], name: str | None = None
|
|
115
123
|
) -> asyncio.Task[Any]:
|
|
116
124
|
pass
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class BaseClient(ClientProtocol):
|
|
128
|
+
def _setup_logger(self) -> None:
|
|
129
|
+
if not self.logger.handlers:
|
|
130
|
+
if not self.logger.level:
|
|
131
|
+
self.logger.setLevel(logging.INFO)
|
|
132
|
+
handler = logging.StreamHandler()
|
|
133
|
+
formatter = ColoredFormatter(
|
|
134
|
+
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
135
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
136
|
+
)
|
|
137
|
+
handler.setFormatter(formatter)
|
|
138
|
+
self.logger.addHandler(handler)
|
|
139
|
+
|
|
140
|
+
async def _safe_execute(self, coro, *, context: str = "unknown") -> Any:
|
|
141
|
+
try:
|
|
142
|
+
return await coro
|
|
143
|
+
except Exception as e:
|
|
144
|
+
self.logger.error(f"Unhandled exception in {context}: {e}\n{traceback.format_exc()}")
|
|
145
|
+
|
|
146
|
+
def _create_safe_task(
|
|
147
|
+
self, coro: Awaitable[Any], name: str | None = None
|
|
148
|
+
) -> asyncio.Task[Any | None]:
|
|
149
|
+
async def runner():
|
|
150
|
+
try:
|
|
151
|
+
return await coro
|
|
152
|
+
except asyncio.CancelledError:
|
|
153
|
+
raise
|
|
154
|
+
except Exception as e:
|
|
155
|
+
tb = traceback.format_exc()
|
|
156
|
+
self.logger.error(f"Unhandled exception in task {name or coro}: {e}\n{tb}")
|
|
157
|
+
raise
|
|
158
|
+
|
|
159
|
+
task = asyncio.create_task(runner(), name=name)
|
|
160
|
+
self._background_tasks.add(task)
|
|
161
|
+
return task
|
|
162
|
+
|
|
163
|
+
async def _cleanup_client(self) -> None:
|
|
164
|
+
for task in list(self._background_tasks):
|
|
165
|
+
task.cancel()
|
|
166
|
+
try:
|
|
167
|
+
await task
|
|
168
|
+
except asyncio.CancelledError:
|
|
169
|
+
pass
|
|
170
|
+
except Exception:
|
|
171
|
+
self.logger.debug("Background task raised during cancellation", exc_info=True)
|
|
172
|
+
self._background_tasks.discard(task)
|
|
173
|
+
|
|
174
|
+
if self._recv_task:
|
|
175
|
+
self._recv_task.cancel()
|
|
176
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
177
|
+
await self._recv_task
|
|
178
|
+
self._recv_task = None
|
|
179
|
+
|
|
180
|
+
if self._outgoing_task:
|
|
181
|
+
self._outgoing_task.cancel()
|
|
182
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
183
|
+
await self._outgoing_task
|
|
184
|
+
self._outgoing_task = None
|
|
185
|
+
|
|
186
|
+
for fut in self._pending.values():
|
|
187
|
+
if not fut.done():
|
|
188
|
+
fut.set_exception(WebSocketNotConnectedError())
|
|
189
|
+
self._pending.clear()
|
|
190
|
+
|
|
191
|
+
if self._ws:
|
|
192
|
+
try:
|
|
193
|
+
await self._ws.close()
|
|
194
|
+
except Exception:
|
|
195
|
+
self.logger.debug("Error closing ws during cleanup", exc_info=True)
|
|
196
|
+
self._ws = None
|
|
197
|
+
|
|
198
|
+
self.is_connected = False
|
|
199
|
+
self.logger.info("Client start() cleaned up")
|
|
200
|
+
|
|
201
|
+
async def idle(self):
|
|
202
|
+
"""
|
|
203
|
+
Поддерживает клиента в «ожидающем» состоянии до закрытия клиента или иного прерывающего события.
|
|
204
|
+
|
|
205
|
+
:return: Никогда не возвращает значение; функция блокирует выполнение.
|
|
206
|
+
:rtype: None
|
|
207
|
+
"""
|
|
208
|
+
await asyncio.Event().wait()
|
|
209
|
+
|
|
210
|
+
def inspect(self) -> None:
|
|
211
|
+
"""
|
|
212
|
+
Выводит в лог текущий статус клиента для отладки.
|
|
213
|
+
"""
|
|
214
|
+
self.logger.info("Pymax")
|
|
215
|
+
self.logger.info("---------")
|
|
216
|
+
self.logger.info(f"Connected: {self.is_connected}")
|
|
217
|
+
if self.me is not None:
|
|
218
|
+
self.logger.info(f"Me: {self.me.names[0].first_name} ({self.me.id})")
|
|
219
|
+
else:
|
|
220
|
+
self.logger.info("Me: N/A")
|
|
221
|
+
self.logger.info(f"Dialogs: {len(self.dialogs)}")
|
|
222
|
+
self.logger.info(f"Chats: {len(self.chats)}")
|
|
223
|
+
self.logger.info(f"Channels: {len(self.channels)}")
|
|
224
|
+
self.logger.info(f"Users cached: {len(self._users)}")
|
|
225
|
+
self.logger.info(f"Background tasks: {len(self._background_tasks)}")
|
|
226
|
+
self.logger.info(f"Scheduled tasks: {len(self._scheduled_tasks)}")
|
|
227
|
+
self.logger.info("---------")
|
|
228
|
+
|
|
229
|
+
async def __aenter__(self) -> Self:
|
|
230
|
+
self._create_safe_task(self.start(), name="start")
|
|
231
|
+
while not self.is_connected:
|
|
232
|
+
await asyncio.sleep(0.05)
|
|
233
|
+
return self
|
|
234
|
+
|
|
235
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
236
|
+
await self.close()
|
|
237
|
+
|
|
238
|
+
@abstractmethod
|
|
239
|
+
async def login_with_code(self, temp_token: str, code: str, start: bool = False) -> None:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
@abstractmethod
|
|
243
|
+
async def _post_login_tasks(self, sync: bool = True) -> None:
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
@abstractmethod
|
|
247
|
+
async def _wait_forever(self) -> None:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
@abstractmethod
|
|
251
|
+
async def start(self) -> None:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
@abstractmethod
|
|
255
|
+
async def close(self) -> None:
|
|
256
|
+
pass
|
pymax/mixins/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@ from .channel import ChannelMixin
|
|
|
3
3
|
from .group import GroupMixin
|
|
4
4
|
from .handler import HandlerMixin
|
|
5
5
|
from .message import MessageMixin
|
|
6
|
+
from .scheduler import SchedulerMixin
|
|
6
7
|
from .self import SelfMixin
|
|
7
8
|
from .socket import SocketMixin
|
|
8
9
|
from .telemetry import TelemetryMixin
|
|
@@ -19,6 +20,7 @@ class ApiMixin(
|
|
|
19
20
|
MessageMixin,
|
|
20
21
|
TelemetryMixin,
|
|
21
22
|
GroupMixin,
|
|
23
|
+
SchedulerMixin,
|
|
22
24
|
):
|
|
23
25
|
pass
|
|
24
26
|
|
|
@@ -29,6 +31,7 @@ __all__ = [
|
|
|
29
31
|
"ChannelMixin",
|
|
30
32
|
"HandlerMixin",
|
|
31
33
|
"MessageMixin",
|
|
34
|
+
"SchedulerMixin",
|
|
32
35
|
"SelfMixin",
|
|
33
36
|
"SocketMixin",
|
|
34
37
|
"TelemetryMixin",
|