maxapi-python 2.4.0__py3-none-any.whl → 2.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. {maxapi_python-2.4.0.dist-info → maxapi_python-2.5.0.dist-info}/METADATA +22 -12
  2. maxapi_python-2.5.0.dist-info/RECORD +195 -0
  3. {maxapi_python-2.4.0.dist-info → maxapi_python-2.5.0.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +8 -2
  5. pymax/_data/apk_fingerprints.json +374 -17
  6. pymax/_data/rootca_ssl_rsa2022.crt +33 -0
  7. pymax/api/auth/service.py +15 -2
  8. pymax/api/auth/types.py +3 -0
  9. pymax/api/bots/payloads.py +8 -0
  10. pymax/api/bots/service.py +13 -1
  11. pymax/api/chats/enums.py +1 -0
  12. pymax/api/chats/payloads.py +21 -1
  13. pymax/api/chats/service.py +41 -0
  14. pymax/api/facade.py +2 -0
  15. pymax/api/messages/__init__.py +1 -1
  16. pymax/api/messages/enums.py +1 -0
  17. pymax/api/messages/payloads.py +56 -4
  18. pymax/api/messages/service.py +405 -27
  19. pymax/api/response.py +1 -1
  20. pymax/api/stories/__init__.py +2 -0
  21. pymax/api/stories/enums.py +19 -0
  22. pymax/api/stories/payloads.py +64 -0
  23. pymax/api/stories/service.py +198 -0
  24. pymax/api/uploads/payloads.py +42 -1
  25. pymax/api/uploads/service.py +171 -69
  26. pymax/app.py +39 -8
  27. pymax/auth/__init__.py +2 -0
  28. pymax/auth/exceptions.py +9 -0
  29. pymax/auth/providers.py +6 -2
  30. pymax/auth/qr.py +11 -1
  31. pymax/auth/sms.py +11 -1
  32. pymax/base.py +146 -27
  33. pymax/client.py +39 -11
  34. pymax/client_web.py +15 -12
  35. pymax/config.py +101 -53
  36. pymax/di/__init__.py +2 -0
  37. pymax/di/context.py +18 -0
  38. pymax/di/resolver.py +38 -0
  39. pymax/dispatch/__init__.py +7 -0
  40. pymax/dispatch/dispatcher.py +143 -40
  41. pymax/dispatch/enums.py +1 -0
  42. pymax/dispatch/mapping.py +23 -3
  43. pymax/dispatch/middleware.py +32 -0
  44. pymax/dispatch/resolvers.py +4 -0
  45. pymax/dispatch/router.py +86 -34
  46. pymax/files/base.py +58 -1
  47. pymax/files/video.py +32 -40
  48. pymax/files/voice.py +18 -4
  49. pymax/filters/__init__.py +15 -0
  50. pymax/filters/base.py +218 -0
  51. pymax/filters/containers/__init__.py +2 -0
  52. pymax/filters/containers/chat.py +240 -0
  53. pymax/filters/containers/message.py +147 -0
  54. pymax/filters/filters.py +211 -0
  55. pymax/filters/magic.py +13 -0
  56. pymax/filters/presets.py +88 -0
  57. pymax/filters/refs.py +56 -0
  58. pymax/fingerprint/fingerprint.py +5 -22
  59. pymax/fingerprint/models.py +1 -0
  60. pymax/formatting/markdown.py +55 -1
  61. pymax/fsm/__init__.py +3 -0
  62. pymax/fsm/context.py +30 -0
  63. pymax/fsm/protocol.py +14 -0
  64. pymax/fsm/state.py +70 -0
  65. pymax/fsm/storage.py +151 -0
  66. pymax/infra/base.py +2 -0
  67. pymax/infra/bots.py +20 -0
  68. pymax/infra/chat.py +37 -5
  69. pymax/infra/message.py +204 -9
  70. pymax/infra/protocol.py +5 -1
  71. pymax/infra/stories.py +205 -0
  72. pymax/integrations/__init__.py +0 -0
  73. pymax/integrations/dishka.py +109 -0
  74. pymax/protocol/enums.py +2 -0
  75. pymax/protocol/tcp/payload.py +0 -2
  76. pymax/session/__init__.py +1 -1
  77. pymax/session/models.py +30 -0
  78. pymax/session/protocol.py +33 -7
  79. pymax/session/store.py +73 -5
  80. pymax/transport/tcp.py +11 -3
  81. pymax/types/domain/__init__.py +23 -1
  82. pymax/types/domain/attachments/__init__.py +1 -1
  83. pymax/types/domain/attachments/call.py +7 -1
  84. pymax/types/domain/attachments/contact.py +3 -3
  85. pymax/types/domain/attachments/control.py +3 -0
  86. pymax/types/domain/attachments/enums.py +16 -0
  87. pymax/types/domain/attachments/keyboards/__init__.py +1 -1
  88. pymax/types/domain/attachments/keyboards/enums.py +12 -0
  89. pymax/types/domain/attachments/keyboards/inline.py +74 -3
  90. pymax/types/domain/attachments/video.py +3 -3
  91. pymax/types/domain/callback.py +19 -0
  92. pymax/types/domain/comment.py +24 -0
  93. pymax/types/domain/enums.py +7 -0
  94. pymax/types/domain/message.py +158 -9
  95. pymax/types/domain/story.py +126 -0
  96. pymax/types/events/reaction.py +28 -4
  97. pymax/versions/__init__.py +2 -0
  98. pymax/versions/catalog.py +104 -0
  99. pymax/versions/exceptions.py +10 -0
  100. maxapi_python-2.4.0.dist-info/RECORD +0 -161
  101. {maxapi_python-2.4.0.dist-info → maxapi_python-2.5.0.dist-info}/licenses/LICENSE +0 -0
pymax/dispatch/router.py CHANGED
@@ -4,17 +4,28 @@ from collections import defaultdict
4
4
  from collections.abc import Awaitable, Callable
5
5
  from dataclasses import dataclass
6
6
  from enum import Enum
7
- from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar
8
-
7
+ from typing import (
8
+ TYPE_CHECKING,
9
+ Any,
10
+ Concatenate,
11
+ Generic,
12
+ ParamSpec,
13
+ Protocol,
14
+ TypeAlias,
15
+ TypeVar,
16
+ )
17
+
18
+ from pymax.filters import Filter
9
19
  from pymax.types import MessageDeleteEvent
10
20
 
11
21
  from .enums import EventType
12
22
 
13
23
  if TYPE_CHECKING:
14
- from pymax import Client
15
24
  from pymax.base import BaseClient
25
+ from pymax.client import Client
26
+ from pymax.dispatch.middleware import Middleware
16
27
  from pymax.protocol import InboundFrame
17
- from pymax.types import Chat
28
+ from pymax.types import Chat, StoryPreview
18
29
  from pymax.types.domain import Message
19
30
  from pymax.types.events import (
20
31
  MessageReadEvent,
@@ -33,19 +44,33 @@ class ErrorScope(str, Enum):
33
44
 
34
45
  _EventT = TypeVar("_EventT")
35
46
  ClientT = TypeVar("ClientT", bound="BaseClient")
47
+ _P = ParamSpec("_P")
36
48
 
37
49
  HandlerCallback: TypeAlias = Callable[
38
50
  [_EventT, ClientT],
39
51
  Awaitable[Any] | Any,
40
52
  ]
41
- HandlerDecorator: TypeAlias = Callable[
42
- [HandlerCallback[_EventT, ClientT]],
43
- HandlerCallback[_EventT, ClientT],
44
- ]
45
- FilterCallback: TypeAlias = Callable[
46
- [_EventT],
47
- Awaitable[bool] | bool,
48
- ]
53
+ # HandlerDecorator: TypeAlias = Callable[
54
+ # [HandlerCallback[_EventT, ClientT]],
55
+ # HandlerCallback[_EventT, ClientT],
56
+ # ]
57
+
58
+ HandlerResult: TypeAlias = Awaitable[Any] | Any
59
+
60
+
61
+ class HandlerDecorator(Protocol, Generic[_EventT, ClientT]):
62
+ def __call__(
63
+ self,
64
+ handler: Callable[
65
+ Concatenate[_EventT, ClientT, _P],
66
+ HandlerResult,
67
+ ],
68
+ ) -> Callable[
69
+ Concatenate[_EventT, ClientT, _P],
70
+ HandlerResult,
71
+ ]: ...
72
+
73
+
49
74
  StartCallback: TypeAlias = Callable[
50
75
  [ClientT],
51
76
  Awaitable[Any] | Any,
@@ -91,7 +116,7 @@ DisconnectDecorator: TypeAlias = Callable[
91
116
  @dataclass(slots=True)
92
117
  class HandlerEntry(Generic[_EventT, ClientT]):
93
118
  callback: HandlerCallback[_EventT, ClientT]
94
- filters: tuple[FilterCallback[_EventT], ...] = ()
119
+ filters: tuple[Filter, ...] = ()
95
120
 
96
121
 
97
122
  @dataclass(slots=True)
@@ -107,20 +132,20 @@ class Router(Generic[ClientT]):
107
132
  """Контейнер обработчиков событий PyMax.
108
133
 
109
134
  Роутер хранит обработчики и фильтры. Когда приходит событие, dispatcher
110
- проходит по root router и его дочерним роутерам, проверяет фильтры и
111
- вызывает подходящие callbacks как ``handler(event, client)``.
135
+ проверяет обработчики в порядке регистрации и вызывает первый подходящий
136
+ в каждом роутере, затем обходит его дочерние роутеры. Первые два аргумента
137
+ обработчика — ``event`` и ``client``; дополнительные обязательные параметры
138
+ разрешаются по аннотациям типов через DI.
112
139
 
113
140
  Example:
114
141
  .. code-block:: python
115
142
 
116
143
  from pymax import Client, Message, Router
144
+ from pymax.filters import Command
117
145
 
118
146
  router = Router[Client]()
119
147
 
120
- def is_start(message: Message) -> bool:
121
- return message.text == "/start"
122
-
123
- @router.on_message(is_start)
148
+ @router.on_message(Command("start"))
124
149
  async def start(message: Message, client: Client) -> None:
125
150
  await message.answer("Привет")
126
151
 
@@ -135,6 +160,7 @@ class Router(Generic[ClientT]):
135
160
  ] = defaultdict(list)
136
161
 
137
162
  self.children: list[Router[ClientT]] = []
163
+ self.middlewares: list[Middleware[ClientT]] = []
138
164
  self.on_start_handlers: list[StartCallback[ClientT]] = []
139
165
  self.error_handlers: list[ErrorEntry[ClientT]] = []
140
166
  self.disconnect_handlers: list[DisconnectCallback] = []
@@ -147,6 +173,8 @@ class Router(Generic[ClientT]):
147
173
 
148
174
  ``GLOBAL``-handler видит ошибки всего дерева подключенных router-ов.
149
175
  ``LOCAL``-handler видит только ошибки своего router-а.
176
+ Callback получает ``handler(exception, context)``. При обработке
177
+ событий сюда передаются ошибки фильтров, middleware и handler-ов.
150
178
  """
151
179
  scope = ErrorScope(scope)
152
180
 
@@ -173,13 +201,15 @@ class Router(Generic[ClientT]):
173
201
  self,
174
202
  event: EventType,
175
203
  /,
176
- *filters: FilterCallback[_EventT],
204
+ *filters: Filter[_EventT, ClientT],
177
205
  ) -> HandlerDecorator[_EventT, ClientT]:
178
206
  """Регистрирует обработчик события по ``EventType``.
179
207
 
180
208
  Args:
181
209
  event: Тип события.
182
- *filters: Фильтры, которые получают событие и возвращают bool.
210
+ *filters: Экземпляры ``Filter``, вызываемые как
211
+ ``filter(event, client)``. Все фильтры должны вернуть ``True``;
212
+ async-результаты ожидаются автоматически.
183
213
 
184
214
  Returns:
185
215
  Декоратор для ``handler(event, client)``.
@@ -187,7 +217,7 @@ class Router(Generic[ClientT]):
187
217
  Example:
188
218
  .. code-block:: python
189
219
 
190
- from pymax import Client
220
+ from pymax import Client, EventType
191
221
  from pymax.protocol import InboundFrame
192
222
 
193
223
  @router.on(EventType.RAW)
@@ -196,8 +226,14 @@ class Router(Generic[ClientT]):
196
226
  """
197
227
 
198
228
  def decorator(
199
- handler: HandlerCallback[_EventT, ClientT],
200
- ) -> HandlerCallback[_EventT, ClientT]:
229
+ handler: Callable[
230
+ Concatenate[_EventT, ClientT, _P],
231
+ HandlerResult,
232
+ ],
233
+ ) -> Callable[
234
+ Concatenate[_EventT, ClientT, _P],
235
+ HandlerResult,
236
+ ]:
201
237
  self.handlers[event].append(
202
238
  HandlerEntry(
203
239
  callback=handler,
@@ -234,7 +270,7 @@ class Router(Generic[ClientT]):
234
270
 
235
271
  def on_message(
236
272
  self,
237
- *filters: FilterCallback[Message],
273
+ *filters: Filter[Message, ClientT],
238
274
  ) -> HandlerDecorator[Message, ClientT]:
239
275
  """Регистрирует обработчик новых сообщений.
240
276
 
@@ -248,7 +284,7 @@ class Router(Generic[ClientT]):
248
284
 
249
285
  def on_message_edit(
250
286
  self,
251
- *filters: FilterCallback[Message],
287
+ *filters: Filter[Message, ClientT],
252
288
  ) -> HandlerDecorator[Message, ClientT]:
253
289
  """Регистрирует обработчик редактирования сообщений.
254
290
 
@@ -262,7 +298,7 @@ class Router(Generic[ClientT]):
262
298
 
263
299
  def on_message_delete(
264
300
  self,
265
- *filters: FilterCallback[MessageDeleteEvent],
301
+ *filters: Filter[MessageDeleteEvent, ClientT],
266
302
  ) -> HandlerDecorator[MessageDeleteEvent, ClientT]:
267
303
  """Регистрирует обработчик удаления сообщений.
268
304
 
@@ -276,35 +312,38 @@ class Router(Generic[ClientT]):
276
312
 
277
313
  def on_message_read(
278
314
  self,
279
- *filters: FilterCallback[MessageReadEvent],
315
+ *filters: Filter[MessageReadEvent, ClientT],
280
316
  ) -> HandlerDecorator[MessageReadEvent, ClientT]:
281
317
  """Регистрирует обработчик изменения отметки прочтения."""
282
318
  return self.on(EventType.MESSAGE_READ, *filters)
283
319
 
284
320
  def on_typing(
285
321
  self,
286
- *filters: FilterCallback[TypingEvent],
322
+ *filters: Filter[TypingEvent, ClientT],
287
323
  ) -> HandlerDecorator[TypingEvent, ClientT]:
288
324
  """Регистрирует обработчик набора текста."""
289
325
  return self.on(EventType.TYPING, *filters)
290
326
 
291
327
  def on_presence(
292
328
  self,
293
- *filters: FilterCallback[PresenceEvent],
329
+ *filters: Filter[PresenceEvent, ClientT],
294
330
  ) -> HandlerDecorator[PresenceEvent, ClientT]:
295
331
  """Регистрирует обработчик изменения присутствия пользователя."""
296
332
  return self.on(EventType.PRESENCE, *filters)
297
333
 
298
334
  def on_reaction_update(
299
335
  self,
300
- *filters: FilterCallback[ReactionUpdateEvent],
336
+ *filters: Filter[ReactionUpdateEvent, ClientT],
301
337
  ) -> HandlerDecorator[ReactionUpdateEvent, ClientT]:
302
- """Регистрирует обработчик обновления реакций сообщения."""
338
+ """Регистрирует обработчик ``ReactionUpdateEvent``.
339
+
340
+ Получает обновления счетчиков реакций и реакции текущего пользователя.
341
+ """
303
342
  return self.on(EventType.REACTION_UPDATE, *filters)
304
343
 
305
344
  def on_chat_update(
306
345
  self,
307
- *filters: FilterCallback[Chat],
346
+ *filters: Filter[Chat, ClientT],
308
347
  ) -> HandlerDecorator[Chat, ClientT]:
309
348
  """Регистрирует обработчик обновления чата.
310
349
 
@@ -316,12 +355,25 @@ class Router(Generic[ClientT]):
316
355
  """
317
356
  return self.on(EventType.CHAT_UPDATE, *filters)
318
357
 
358
+ def on_story_update(
359
+ self,
360
+ *filters: Filter[StoryPreview, ClientT],
361
+ ) -> HandlerDecorator[StoryPreview, ClientT]:
362
+ """Регистрирует обработчик обновления ``StoryPreview`` владельца.
363
+
364
+ Первый аргумент handler-а содержит сводное превью, а не ``Story``.
365
+ """
366
+ return self.on(EventType.STORY_UPDATE, *filters)
367
+
319
368
  def on_raw(
320
369
  self,
321
- *filters: FilterCallback[InboundFrame],
370
+ *filters: Filter[InboundFrame, ClientT],
322
371
  ) -> HandlerDecorator[InboundFrame, ClientT]:
323
372
  """Регистрирует обработчик исходных frame-ов.
324
373
 
374
+ Вызывается после типизированной обработки, даже если тип события
375
+ неизвестен или его обработка завершилась ошибкой.
376
+
325
377
  Args:
326
378
  *filters: Фильтры для ``InboundFrame``.
327
379
 
pymax/files/base.py CHANGED
@@ -1,10 +1,16 @@
1
1
  import os
2
2
  from abc import ABC, abstractmethod
3
3
  from collections.abc import AsyncGenerator
4
+ from io import BytesIO
4
5
 
5
6
  import aiofiles
6
7
  import aiohttp
7
8
 
9
+ try:
10
+ from tinytag import TinyTag
11
+ except ImportError:
12
+ TinyTag = None
13
+
8
14
 
9
15
  class BaseFile(ABC):
10
16
  def __init__(
@@ -56,7 +62,7 @@ class BaseFile(ABC):
56
62
 
57
63
  if self.url:
58
64
  async with aiohttp.ClientSession() as session: # noqa: SIM117
59
- async with session.head(self.url) as resp:
65
+ async with session.head(self.url, allow_redirects=True) as resp:
60
66
  return int(resp.headers["Content-Length"])
61
67
  else:
62
68
  raise ValueError("Path or Url must be provided")
@@ -85,3 +91,54 @@ class BaseFile(ABC):
85
91
  resp.raise_for_status()
86
92
  async for chunk in resp.content.iter_chunked(size):
87
93
  yield chunk
94
+
95
+
96
+ class TimedMediaFile(BaseFile):
97
+ def __init__(
98
+ self,
99
+ raw: bytes | None = None,
100
+ *,
101
+ path: str | None,
102
+ url: str | None,
103
+ name: str | None,
104
+ duration: int | None,
105
+ ) -> None:
106
+ self.duration = duration
107
+ super().__init__(raw, path=path, url=url, name=name)
108
+
109
+ async def get_duration(self) -> int:
110
+ if self.duration is not None:
111
+ return self.duration
112
+
113
+ if not TinyTag:
114
+ raise RuntimeError(
115
+ "Automatic video duration detection requires the 'video' extra. "
116
+ "Install it with `uv add 'maxapi-python[video]'` "
117
+ "or pass duration manually."
118
+ )
119
+
120
+ if self.raw:
121
+ tag = TinyTag.get(
122
+ filename=self.name,
123
+ file_obj=BytesIO(self.raw),
124
+ tags=False,
125
+ duration=True,
126
+ )
127
+ elif self.path:
128
+ tag = TinyTag.get(
129
+ filename=self.path,
130
+ tags=False,
131
+ duration=True,
132
+ )
133
+ else:
134
+ tag = TinyTag.get(
135
+ filename=self.name,
136
+ file_obj=BytesIO(await self.read()),
137
+ tags=False,
138
+ duration=True,
139
+ )
140
+
141
+ if tag.duration is None:
142
+ raise ValueError("Failed to determine video duration.")
143
+
144
+ return round(tag.duration * 1000)
pymax/files/video.py CHANGED
@@ -1,13 +1,7 @@
1
1
  from collections.abc import AsyncGenerator
2
- from io import BytesIO
3
2
  from pathlib import Path
4
3
 
5
- from .base import BaseFile
6
-
7
- try:
8
- from tinytag import TinyTag
9
- except ImportError:
10
- TinyTag = None
4
+ from .base import BaseFile, TimedMediaFile
11
5
 
12
6
 
13
7
  class Video(BaseFile):
@@ -80,12 +74,23 @@ class Video(BaseFile):
80
74
  return super().iter_chunks(size)
81
75
 
82
76
 
83
- class VideoNote(Video):
77
+ class VideoNote(TimedMediaFile):
84
78
  """Круглое видеосообщение для отправки.
85
79
 
86
80
  Принимает те же источники, что и ``Video``. Длительность задается в
87
81
  миллисекундах. Если она не передана, требуется extra ``video`` для
88
82
  автоматического определения длительности.
83
+
84
+ Args:
85
+ raw: Байты видеосообщения.
86
+ url: URL видеосообщения.
87
+ path: Локальный путь к видеосообщению.
88
+ name: Имя файла. Обязательно для ``raw``.
89
+ duration: Длительность в миллисекундах. ``None`` включает
90
+ автоматическое определение через extra ``video``.
91
+
92
+ Raises:
93
+ RuntimeError: При определении длительности без extra ``video``.
89
94
  """
90
95
 
91
96
  def __init__(
@@ -98,35 +103,22 @@ class VideoNote(Video):
98
103
  duration: int | None = None,
99
104
  ) -> None:
100
105
  self.duration = duration
101
- super().__init__(raw, url=url, path=path, name=name)
102
-
103
- async def get_duration(self) -> int:
104
- if self.duration is not None:
105
- return self.duration
106
-
107
- if not TinyTag:
108
- raise RuntimeError(
109
- "Automatic video duration detection requires the 'video' extra. "
110
- "Install it with `uv add 'maxapi-python[video]'` "
111
- "or pass duration manually."
112
- )
113
-
114
- if self.raw:
115
- tag = TinyTag.get(
116
- filename=self.name,
117
- file_obj=BytesIO(self.raw),
118
- tags=False,
119
- duration=True,
120
- )
121
- else:
122
- tag = TinyTag.get(
123
- filename=self.name,
124
- file_obj=BytesIO(await self.read()),
125
- tags=False,
126
- duration=True,
127
- )
128
-
129
- if tag.duration is None:
130
- raise ValueError("Failed to determine video duration.")
131
-
132
- return round(tag.duration * 1000)
106
+ self.name: str = name or ""
107
+ if not self.name and path:
108
+ self.name = Path(path).name
109
+ elif not self.name and url:
110
+ self.name = Path(url).name
111
+
112
+ if not self.name:
113
+ raise ValueError("Either name, url or path must be provided.")
114
+
115
+ super().__init__(raw, url=url, path=path, name=self.name, duration=self.duration)
116
+
117
+ async def read(self) -> bytes:
118
+ return await super().read()
119
+
120
+ def iter_chunks(self, size: int) -> AsyncGenerator[bytes, None]:
121
+ return super().iter_chunks(size)
122
+
123
+ async def size(self) -> int:
124
+ return await super().size()
pymax/files/voice.py CHANGED
@@ -1,14 +1,26 @@
1
1
  from collections.abc import AsyncGenerator
2
2
  from pathlib import Path
3
3
 
4
- from .base import BaseFile
4
+ from .base import TimedMediaFile
5
5
 
6
6
 
7
- class Voice(BaseFile):
7
+ class Voice(TimedMediaFile):
8
8
  """Голосовое сообщение в формате OGG для отправки.
9
9
 
10
10
  Принимает ``path``, ``url`` или ``raw``. Для ``raw`` необходимо явно
11
- передать имя файла.
11
+ передать имя файла. Длительность задается в миллисекундах; если она не
12
+ передана, требуется extra ``video`` для автоматического определения.
13
+
14
+ Args:
15
+ raw: Байты голосового сообщения.
16
+ path: Локальный путь к OGG-файлу.
17
+ url: URL OGG-файла.
18
+ name: Имя файла. Обязательно для ``raw``.
19
+ duration: Длительность в миллисекундах. ``None`` включает
20
+ автоматическое определение через extra ``video``.
21
+
22
+ Raises:
23
+ RuntimeError: При определении длительности без extra ``video``.
12
24
  """
13
25
 
14
26
  def __init__(
@@ -18,8 +30,10 @@ class Voice(BaseFile):
18
30
  path: str | None = None,
19
31
  url: str | None = None,
20
32
  name: str | None = None,
33
+ duration: int | None = None,
21
34
  ) -> None:
22
35
  self.name: str = name or ""
36
+ self.duration = duration
23
37
  if not self.name and path:
24
38
  self.name = Path(path).name
25
39
  elif not self.name and url:
@@ -27,7 +41,7 @@ class Voice(BaseFile):
27
41
 
28
42
  if not self.name:
29
43
  raise ValueError("Either name, url or path must be provided.")
30
- super().__init__(raw=raw, url=url, path=path, name=self.name)
44
+ super().__init__(raw=raw, url=url, path=path, name=self.name, duration=self.duration)
31
45
 
32
46
  async def read(self) -> bytes:
33
47
  return await super().read()
@@ -0,0 +1,15 @@
1
+ from .base import Filter
2
+ from .magic import MagicFilter
3
+ from .presets import Command, Forwarded, FromMe, HasAttachment, Reply
4
+
5
+ F = MagicFilter()
6
+
7
+ __all__ = (
8
+ "Command",
9
+ "Filter",
10
+ "Forwarded",
11
+ "FromMe",
12
+ "HasAttachment",
13
+ "MagicFilter",
14
+ "Reply",
15
+ )