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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxapi-python
3
- Version: 1.1.20
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
- phone = "+1234567890"
86
- client = MaxClient(phone=phone, work_dir="cache")
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 handle_start() -> None:
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
- if history:
101
- for message in history:
102
- user = await client.get_user(message.sender)
103
- if user:
104
- print(f"{user.names[0].name}: {message.text}")
105
-
106
- async def main() -> None:
107
- await client.start()
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
- [WIP](https://ink-developer.github.io/PyMax)
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 Awaitable, Callable
31
- from typing import Any
33
+ from collections.abc import Callable
32
34
 
33
35
  import websockets
34
36
 
35
- from .filters import Filter
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
- Args:
48
- phone (str): Номер телефона для авторизации.
49
- uri (str, optional): URI WebSocket сервера. По умолчанию Constants.WEBSOCKET_URI.value.
50
- work_dir (str, optional): Рабочая директория для хранения базы данных. По умолчанию ".".
51
- logger (logging.Logger | None): Пользовательский логгер. Если не передан — используется
52
- логгер модуля с именем f"{__name__}.MaxClient".
53
- headers (UserAgentPayload): Заголовки для подключения к WebSocket.
54
- token (str | None, optional): Токен авторизации. Если не передан, будет выполнен
55
- процесс логина по номеру телефона.
56
- host (str, optional): Хост API сервера. По умолчанию Constants.HOST.value.
57
- port (int, optional): Порт API сервера. По умолчанию Constants.PORT.value.
58
- registration (bool, optional): Флаг регистрации нового пользователя. По умолчанию False.
59
- first_name (str, optional): Имя пользователя для регистрации. Требуется, если registration=True.
60
- last_name (str | None, optional): Фамилия пользователя для регистрации.
61
- send_fake_telemetry (bool, optional): Флаг отправки фейковой телеметрии. По умолчанию True.
62
- proxy (str | Literal[True] | None, optional): Прокси для подключения к WebSocket.
63
- (См. https://websockets.readthedocs.io/en/stable/topics/proxies.html).
64
- reconnect (bool, optional): Флаг автоматического переподключения при потере соединения. По умолчанию True.
65
-
66
-
67
- Raises:
68
- InvalidPhoneError: Если формат номера телефона неверный.
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 = self._database.get_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], Filter | None]
155
+ tuple[Callable[[Message], Any], BaseFilter[Message] | None]
142
156
  ] = []
143
157
  self._on_message_edit_handlers: list[
144
- tuple[Callable[[Message], Any], Filter | None]
158
+ tuple[Callable[[Message], Any], BaseFilter[Message] | None]
145
159
  ] = []
146
160
  self._on_message_delete_handlers: list[
147
- tuple[Callable[[Message], Any], Filter | None]
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
- self.logger.exception(
234
- f"Unhandled exception in task {name or coro}: {e}",
235
- exc_info=e,
258
+ tb = traceback.format_exc()
259
+ self.logger.error(
260
+ f"Unhandled exception in task {name or coro}: {e}\n{tb}"
236
261
  )
237
- return None
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
- if self._on_start_handler:
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
- Args:
309
- temp_token (str): Временный токен, полученный из request_code()
310
- code (str): Код, введённый пользователем
311
-
312
- Returns:
313
- str: Токен для входа
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 = cast(
35
- UUID | None, session.exec(select(Auth.device_id)).first()
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: UUID, token: str) -> None:
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 .static.enum import MessageStatus, MessageType
2
- from .types import Message
3
-
4
-
5
- class Filter:
6
- def __init__(
7
- self,
8
- chat_id: int | None = None,
9
- user_id: int | None = None,
10
- text: list[str] | None = None,
11
- status: MessageStatus | str | None = None,
12
- type: MessageType | str | None = None,
13
- text_contains: str | None = None,
14
- reaction_info: bool | None = None,
15
- ) -> None:
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
- self.user_id = user_id
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 match(self, message: Message) -> bool:
25
- if self.chat_id is not None and message.chat_id != self.chat_id:
26
- return False
27
- if self.user_id is not None and message.sender != self.user_id:
28
- return False
29
- if self.text is not None and any(
30
- text not in message.text for text in self.text
31
- ):
32
- return False
33
- if (
34
- self.text_contains is not None
35
- and self.text_contains not in message.text
36
- ):
37
- return False
38
- if self.status is not None and message.status != self.status:
39
- return False
40
- if self.type is not None and message.type != self.type:
41
- return False
42
- if (
43
- self.reaction_info is not None and message.reactionInfo is None
44
- ): # noqa: SIM103
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
- return True
162
+ @staticmethod
163
+ def has_file() -> BaseFilter[Message]:
164
+ return FileFilter()
pymax/formatter.py CHANGED
@@ -27,4 +27,5 @@ class ColoredFormatter(logging.Formatter):
27
27
  f"{name_color}{record.name}{self.RESET}: "
28
28
  f"{message_color}{record.getMessage()}{self.RESET}"
29
29
  )
30
+
30
31
  return log