maxapi-python 1.2.3__py3-none-any.whl → 1.2.5__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.2.3.dist-info → maxapi_python-1.2.5.dist-info}/METADATA +9 -7
- maxapi_python-1.2.5.dist-info/RECORD +33 -0
- pymax/core.py +63 -38
- pymax/files.py +28 -7
- pymax/interfaces.py +417 -116
- pymax/mixins/auth.py +231 -5
- pymax/mixins/channel.py +3 -5
- pymax/mixins/group.py +2 -2
- pymax/mixins/handler.py +4 -10
- pymax/mixins/message.py +64 -88
- pymax/mixins/scheduler.py +1 -1
- pymax/mixins/self.py +2 -2
- pymax/mixins/socket.py +16 -340
- pymax/mixins/telemetry.py +10 -6
- pymax/mixins/user.py +3 -5
- pymax/mixins/websocket.py +16 -365
- pymax/payloads.py +44 -1
- pymax/protocols.py +123 -0
- pymax/static/constant.py +76 -8
- pymax/static/enum.py +65 -52
- pymax/types.py +25 -0
- pymax/utils.py +90 -0
- maxapi_python-1.2.3.dist-info/RECORD +0 -32
- pymax/mixins/utils.py +0 -27
- {maxapi_python-1.2.3.dist-info → maxapi_python-1.2.5.dist-info}/WHEEL +0 -0
- {maxapi_python-1.2.3.dist-info → maxapi_python-1.2.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maxapi-python
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.5
|
|
4
4
|
Summary: Python wrapper для API мессенджера Max
|
|
5
|
-
Project-URL: Homepage, https://github.com/
|
|
6
|
-
Project-URL: Repository, https://github.com/
|
|
7
|
-
Project-URL: Issues, https://github.com/
|
|
5
|
+
Project-URL: Homepage, https://github.com/MaxApiTeam/PyMax
|
|
6
|
+
Project-URL: Repository, https://github.com/MaxApiTeam/PyMax
|
|
7
|
+
Project-URL: Issues, https://github.com/MaxApiTeam/PyMax/issues
|
|
8
8
|
Author-email: ink <mail@gmail.com>
|
|
9
9
|
License-Expression: MIT
|
|
10
10
|
License-File: LICENSE
|
|
@@ -18,6 +18,7 @@ Requires-Dist: lz4>=4.4.4
|
|
|
18
18
|
Requires-Dist: msgpack>=1.1.1
|
|
19
19
|
Requires-Dist: qrcode>=8.2
|
|
20
20
|
Requires-Dist: sqlmodel>=0.0.24
|
|
21
|
+
Requires-Dist: ua-generator>=2.0.19
|
|
21
22
|
Requires-Dist: websockets>=15.0
|
|
22
23
|
Provides-Extra: test
|
|
23
24
|
Requires-Dist: flake8; extra == 'test'
|
|
@@ -36,6 +37,7 @@ Description-Content-Type: text/markdown
|
|
|
36
37
|
<strong>Python wrapper для API мессенджера Max</strong>
|
|
37
38
|
</p>
|
|
38
39
|
|
|
40
|
+
|
|
39
41
|
<p align="center">
|
|
40
42
|
<img src="https://img.shields.io/badge/python-3.10+-3776AB.svg" alt="Python 3.11+">
|
|
41
43
|
<img src="https://img.shields.io/badge/License-MIT-2f9872.svg" alt="License: MIT">
|
|
@@ -92,12 +94,12 @@ uv add -U maxapi-python
|
|
|
92
94
|
**Вход по номеру телефона (DESKTOP):**
|
|
93
95
|
|
|
94
96
|
```python
|
|
95
|
-
from pymax import
|
|
97
|
+
from pymax import SocketMaxClient
|
|
96
98
|
from pymax.payloads import UserAgentPayload
|
|
97
99
|
|
|
98
100
|
ua = UserAgentPayload(device_type="DESKTOP", app_version="25.12.13")
|
|
99
101
|
|
|
100
|
-
client =
|
|
102
|
+
client = SocketMaxClient(
|
|
101
103
|
phone="+79111111111",
|
|
102
104
|
work_dir="cache",
|
|
103
105
|
headers=ua,
|
|
@@ -195,6 +197,6 @@ if __name__ == "__main__":
|
|
|
195
197
|
|
|
196
198
|
Спасибо всем за помощь в разработке!
|
|
197
199
|
|
|
198
|
-
<a href="https://github.com/
|
|
200
|
+
<a href="https://github.com/MaxApiTeam/PyMax/graphs/contributors">
|
|
199
201
|
<img src="https://contrib.rocks/image?repo=ink-developer/PyMax" />
|
|
200
202
|
</a>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
|
|
2
|
+
pymax/core.py,sha256=rJKUFlFjdCPywkOP5-lFUwIMnxNHk4oJLf1VYfEhqak,16137
|
|
3
|
+
pymax/crud.py,sha256=YC92TyhA2mr1tJCcfd-tvh8umtXKgqJfgiLo7nXUl3Q,3076
|
|
4
|
+
pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
|
|
5
|
+
pymax/files.py,sha256=nx7oZfIJ8ZvO-TuG5LzSmk8esbBtNrkKdFQgTQVbUA8,4063
|
|
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=GXHi4TjmXPb60KtLXe7CduQ8hSIVHXhA5Ak1OcvSS2w,19793
|
|
10
|
+
pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
|
|
11
|
+
pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
|
|
12
|
+
pymax/payloads.py,sha256=-hUgJYwCFfWFcWedAGYg1v82NliEfoTEteEEI7sgUEQ,8756
|
|
13
|
+
pymax/protocols.py,sha256=PoNvri9jFH6WBXGwugrkU6lwtwJEw0DO2s13HOH8_KI,4025
|
|
14
|
+
pymax/types.py,sha256=z1HXNl8CP_X3jTUlENlF9_vzZKdb7gF5PHG5d4rG3BY,37209
|
|
15
|
+
pymax/utils.py,sha256=HK6E6UYyjtUoJ2KXWeDycyiXm_9j5shZme6VFA2ixeM,2960
|
|
16
|
+
pymax/mixins/__init__.py,sha256=5sXJME34S1EssuDETaN4DLRH7vhMw_Q3Jmay9myAIZM,775
|
|
17
|
+
pymax/mixins/auth.py,sha256=f6IH_gvwB8hLvj7hNI21vgVhR1kju7ABRNjcRS656_o,22843
|
|
18
|
+
pymax/mixins/channel.py,sha256=Qi5ujw5X7QYx4Lq1XnvlJ3BYjmTFmYDfu7_jRcx4Mx8,5335
|
|
19
|
+
pymax/mixins/group.py,sha256=6bsWSx0ULZonDM_dJSC0EkqpZWd6tv9lmmKWj_gEaaw,15903
|
|
20
|
+
pymax/mixins/handler.py,sha256=duL3Q5Bvv8tjUhOKaDr_k7w09BeCwjVdws2ga7v_zNE,12432
|
|
21
|
+
pymax/mixins/message.py,sha256=MpUED92iWONkJRjb0f7lwPj9gYJhDuv0KKzbveOEaAk,33397
|
|
22
|
+
pymax/mixins/scheduler.py,sha256=K4HB9IfksnXPujJnkipIS5um9nuzC8EjbtQn65RtbfI,963
|
|
23
|
+
pymax/mixins/self.py,sha256=Wn9l3zDF5VzFzzisryOQknf3Ngl81Q98_9jqqbE9ZAw,9174
|
|
24
|
+
pymax/mixins/socket.py,sha256=0WCK9FDzkfgRpKeTfCB9GwuNkccww3RTI7P-W8_409c,10904
|
|
25
|
+
pymax/mixins/telemetry.py,sha256=9gbzEXTp9W9BLJ1wJVXOaHU7Q4inL981b-xq93xJyrw,3913
|
|
26
|
+
pymax/mixins/user.py,sha256=Xwb2fWM8RCq0SbVhlRyr1RBQGyjlaImtp0lT2PbgEqE,9420
|
|
27
|
+
pymax/mixins/websocket.py,sha256=CvrfMv0iNYpPtfwZXKm0V-ZOlSohtXZ7QH5fDET5Pec,5223
|
|
28
|
+
pymax/static/constant.py,sha256=hZby1gZmzWTzDgwW_EX1NGgqwdQxwcjl7pOE0thfxT0,2304
|
|
29
|
+
pymax/static/enum.py,sha256=YWNzDfPqSBBT8w9m3XesedjiTETn1juJn5UEOZ3zMe0,5477
|
|
30
|
+
maxapi_python-1.2.5.dist-info/METADATA,sha256=BbvzqfDdavSaBRJSDJ0DqxV_f5Nrzs-2-V0LXMlV52o,6790
|
|
31
|
+
maxapi_python-1.2.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
32
|
+
maxapi_python-1.2.5.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
|
|
33
|
+
maxapi_python-1.2.5.dist-info/RECORD,,
|
pymax/core.py
CHANGED
|
@@ -17,15 +17,12 @@ from .crud import Database
|
|
|
17
17
|
from .exceptions import (
|
|
18
18
|
InvalidPhoneError,
|
|
19
19
|
SocketNotConnectedError,
|
|
20
|
+
WebSocketNotConnectedError,
|
|
20
21
|
)
|
|
21
22
|
from .interfaces import BaseClient
|
|
22
23
|
from .mixins import ApiMixin, SocketMixin, WebSocketMixin
|
|
23
24
|
from .payloads import UserAgentPayload
|
|
24
|
-
from .static.constant import
|
|
25
|
-
HOST,
|
|
26
|
-
PORT,
|
|
27
|
-
WEBSOCKET_URI,
|
|
28
|
-
)
|
|
25
|
+
from .static.constant import HOST, PORT, SESSION_STORAGE_DB, WEBSOCKET_URI
|
|
29
26
|
|
|
30
27
|
if TYPE_CHECKING:
|
|
31
28
|
from collections.abc import Callable
|
|
@@ -34,7 +31,6 @@ if TYPE_CHECKING:
|
|
|
34
31
|
|
|
35
32
|
from pymax.filters import BaseFilter
|
|
36
33
|
|
|
37
|
-
from .filters import Filters
|
|
38
34
|
from .types import Channel, Chat, Dialog, Me, Message, ReactionInfo, User
|
|
39
35
|
|
|
40
36
|
|
|
@@ -42,6 +38,7 @@ logger = logging.getLogger(__name__)
|
|
|
42
38
|
|
|
43
39
|
|
|
44
40
|
class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
41
|
+
allowed_device_types: set[str] = {"WEB"}
|
|
45
42
|
"""
|
|
46
43
|
Основной клиент для работы с WebSocket API сервиса Max.
|
|
47
44
|
|
|
@@ -49,6 +46,8 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
49
46
|
:type phone: str
|
|
50
47
|
:param uri: URI WebSocket сервера.
|
|
51
48
|
:type uri: str, optional
|
|
49
|
+
:param session_name: Название сессии для хранения базы данных.
|
|
50
|
+
:type session_name: str, optional
|
|
52
51
|
:param work_dir: Рабочая директория для хранения базы данных.
|
|
53
52
|
:type work_dir: str, optional
|
|
54
53
|
:param logger: Пользовательский логгер. Если не передан, используется логгер модуля с именем f"{__name__}.MaxClient".
|
|
@@ -81,7 +80,8 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
81
80
|
self,
|
|
82
81
|
phone: str,
|
|
83
82
|
uri: str = WEBSOCKET_URI,
|
|
84
|
-
|
|
83
|
+
session_name: str = SESSION_STORAGE_DB,
|
|
84
|
+
headers: UserAgentPayload | None = None,
|
|
85
85
|
token: str | None = None,
|
|
86
86
|
send_fake_telemetry: bool = True,
|
|
87
87
|
host: str = HOST,
|
|
@@ -120,7 +120,7 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
120
120
|
self._users: dict[int, User] = {}
|
|
121
121
|
|
|
122
122
|
self._work_dir: str = work_dir
|
|
123
|
-
self._database_path: Path = Path(work_dir) /
|
|
123
|
+
self._database_path: Path = Path(work_dir) / session_name
|
|
124
124
|
self._database_path.parent.mkdir(parents=True, exist_ok=True)
|
|
125
125
|
self._database_path.touch(exist_ok=True)
|
|
126
126
|
self._database = Database(self._work_dir)
|
|
@@ -132,6 +132,7 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
132
132
|
self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
133
133
|
self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
134
134
|
self._background_tasks: set[asyncio.Task[Any]] = set()
|
|
135
|
+
self._stop_event = asyncio.Event()
|
|
135
136
|
|
|
136
137
|
self._seq: int = 0
|
|
137
138
|
self._error_count: int = 0
|
|
@@ -142,7 +143,10 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
142
143
|
self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
143
144
|
|
|
144
145
|
self._token = self._database.get_auth_token() or token
|
|
146
|
+
if headers is None:
|
|
147
|
+
headers = self._default_headers()
|
|
145
148
|
self.user_agent = headers
|
|
149
|
+
self._validate_device_type()
|
|
146
150
|
self._send_fake_telemetry: bool = send_fake_telemetry
|
|
147
151
|
self._session_id: int = int(time.time() * 1000)
|
|
148
152
|
self._action_id: int = 1
|
|
@@ -180,11 +184,24 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
180
184
|
self._work_dir,
|
|
181
185
|
)
|
|
182
186
|
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _default_headers() -> UserAgentPayload:
|
|
189
|
+
return UserAgentPayload(device_type="WEB")
|
|
190
|
+
|
|
191
|
+
def _validate_device_type(self) -> None:
|
|
192
|
+
if self.user_agent.device_type not in self.allowed_device_types:
|
|
193
|
+
raise ValueError(
|
|
194
|
+
f"{self.__class__.__name__} does not support "
|
|
195
|
+
f"device_type={self.user_agent.device_type}"
|
|
196
|
+
)
|
|
197
|
+
|
|
183
198
|
async def _wait_forever(self) -> None:
|
|
184
199
|
try:
|
|
185
200
|
await self.ws.wait_closed()
|
|
186
201
|
except asyncio.CancelledError:
|
|
187
202
|
self.logger.debug("wait_closed cancelled")
|
|
203
|
+
except WebSocketNotConnectedError:
|
|
204
|
+
self.logger.info("WebSocket not connected, exiting wait_forever")
|
|
188
205
|
|
|
189
206
|
async def close(self) -> None:
|
|
190
207
|
"""
|
|
@@ -194,22 +211,7 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
194
211
|
"""
|
|
195
212
|
try:
|
|
196
213
|
self.logger.info("Closing client")
|
|
197
|
-
|
|
198
|
-
self._recv_task.cancel()
|
|
199
|
-
try:
|
|
200
|
-
await self._recv_task
|
|
201
|
-
except asyncio.CancelledError:
|
|
202
|
-
self.logger.debug("recv_task cancelled")
|
|
203
|
-
if self._outgoing_task:
|
|
204
|
-
self._outgoing_task.cancel()
|
|
205
|
-
try:
|
|
206
|
-
await self._outgoing_task
|
|
207
|
-
except asyncio.CancelledError:
|
|
208
|
-
self.logger.debug("outgoing_task cancelled")
|
|
209
|
-
if self._ws:
|
|
210
|
-
await self._ws.close()
|
|
211
|
-
self.is_connected = False
|
|
212
|
-
self.logger.info("Client closed")
|
|
214
|
+
self._stop_event.set()
|
|
213
215
|
except Exception:
|
|
214
216
|
self.logger.exception("Error closing client")
|
|
215
217
|
|
|
@@ -250,7 +252,15 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
250
252
|
:rtype: None
|
|
251
253
|
"""
|
|
252
254
|
resp = await self._send_code(code, temp_token)
|
|
253
|
-
|
|
255
|
+
|
|
256
|
+
login_attrs = resp.get("tokenAttrs", {}).get("LOGIN", {})
|
|
257
|
+
password_challenge = resp.get("passwordChallenge")
|
|
258
|
+
|
|
259
|
+
if password_challenge and not login_attrs:
|
|
260
|
+
token = await self._two_factor_auth(password_challenge)
|
|
261
|
+
else:
|
|
262
|
+
token = login_attrs.get("token")
|
|
263
|
+
|
|
254
264
|
if not token:
|
|
255
265
|
raise ValueError("Login response did not contain tokenAttrs.LOGIN.token")
|
|
256
266
|
self._token = token
|
|
@@ -279,10 +289,9 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
279
289
|
:return: None
|
|
280
290
|
:rtype: None
|
|
281
291
|
"""
|
|
282
|
-
|
|
283
|
-
while
|
|
292
|
+
self.logger.info("Client starting")
|
|
293
|
+
while not self._stop_event.is_set():
|
|
284
294
|
try:
|
|
285
|
-
self.logger.info("Client starting")
|
|
286
295
|
await self.connect(self.user_agent)
|
|
287
296
|
|
|
288
297
|
if self.registration:
|
|
@@ -297,29 +306,45 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
297
306
|
await self._login()
|
|
298
307
|
|
|
299
308
|
await self._sync(self.user_agent)
|
|
300
|
-
|
|
301
309
|
await self._post_login_tasks(sync=False)
|
|
302
310
|
|
|
303
|
-
|
|
304
|
-
self.
|
|
311
|
+
wait_task = asyncio.create_task(self._wait_forever())
|
|
312
|
+
stop_task = asyncio.create_task(self._stop_event.wait())
|
|
313
|
+
|
|
314
|
+
done, pending = await asyncio.wait(
|
|
315
|
+
[wait_task, stop_task], return_when=asyncio.FIRST_COMPLETED
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
for task in pending:
|
|
319
|
+
task.cancel()
|
|
320
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
321
|
+
await task
|
|
322
|
+
|
|
323
|
+
except asyncio.CancelledError:
|
|
324
|
+
self.logger.info("Client task cancelled, stopping")
|
|
325
|
+
break
|
|
305
326
|
except Exception as e:
|
|
306
327
|
self.logger.exception("Client start iteration failed")
|
|
307
|
-
raise e
|
|
308
|
-
|
|
309
328
|
finally:
|
|
310
|
-
self.logger.debug("Cleaning up background tasks and pending futures")
|
|
311
|
-
|
|
312
329
|
await self._cleanup_client()
|
|
313
330
|
|
|
314
|
-
if not self.reconnect:
|
|
315
|
-
self.logger.info("Reconnect disabled — exiting start()")
|
|
316
|
-
|
|
331
|
+
if not self.reconnect or self._stop_event.is_set():
|
|
332
|
+
self.logger.info("Reconnect disabled or stop requested — exiting start()")
|
|
333
|
+
break
|
|
317
334
|
|
|
318
335
|
self.logger.info("Reconnect enabled — restarting client")
|
|
319
336
|
await asyncio.sleep(self.reconnect_delay)
|
|
320
337
|
|
|
338
|
+
self.logger.info("Client exited cleanly")
|
|
339
|
+
|
|
321
340
|
|
|
322
341
|
class SocketMaxClient(SocketMixin, MaxClient):
|
|
342
|
+
allowed_device_types = {"ANDROID", "IOS", "DESKTOP"}
|
|
343
|
+
|
|
344
|
+
@staticmethod
|
|
345
|
+
def _default_headers() -> UserAgentPayload:
|
|
346
|
+
return UserAgentPayload(device_type="DESKTOP")
|
|
347
|
+
|
|
323
348
|
@override
|
|
324
349
|
async def _wait_forever(self):
|
|
325
350
|
if self._recv_task:
|
pymax/files.py
CHANGED
|
@@ -9,7 +9,10 @@ from typing_extensions import override
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class BaseFile(ABC):
|
|
12
|
-
def __init__(
|
|
12
|
+
def __init__(
|
|
13
|
+
self, raw: bytes | None = None, *, url: str | None = None, path: str | None = None
|
|
14
|
+
) -> None:
|
|
15
|
+
self.raw = raw
|
|
13
16
|
self.url = url
|
|
14
17
|
self.path = path
|
|
15
18
|
|
|
@@ -21,6 +24,9 @@ class BaseFile(ABC):
|
|
|
21
24
|
|
|
22
25
|
@abstractmethod
|
|
23
26
|
async def read(self) -> bytes:
|
|
27
|
+
if self.raw is not None:
|
|
28
|
+
return self.raw
|
|
29
|
+
|
|
24
30
|
if self.url:
|
|
25
31
|
async with (
|
|
26
32
|
ClientSession() as session,
|
|
@@ -45,13 +51,24 @@ class Photo(BaseFile):
|
|
|
45
51
|
".bmp",
|
|
46
52
|
} # FIXME: костыль ✅
|
|
47
53
|
|
|
48
|
-
def __init__(
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
raw: bytes | None = None,
|
|
57
|
+
*,
|
|
58
|
+
url: str | None = None,
|
|
59
|
+
path: str | None = None,
|
|
60
|
+
name: str | None = None,
|
|
61
|
+
) -> None:
|
|
49
62
|
if path:
|
|
50
63
|
self.file_name = Path(path).name
|
|
51
64
|
elif url:
|
|
52
65
|
self.file_name = Path(url).name
|
|
66
|
+
elif name:
|
|
67
|
+
self.file_name = name
|
|
68
|
+
else:
|
|
69
|
+
self.file_name = ""
|
|
53
70
|
|
|
54
|
-
super().__init__(url, path)
|
|
71
|
+
super().__init__(raw=raw, url=url, path=path)
|
|
55
72
|
|
|
56
73
|
def validate_photo(self) -> tuple[str, str] | None:
|
|
57
74
|
if self.path:
|
|
@@ -83,7 +100,9 @@ class Photo(BaseFile):
|
|
|
83
100
|
|
|
84
101
|
|
|
85
102
|
class Video(BaseFile):
|
|
86
|
-
def __init__(
|
|
103
|
+
def __init__(
|
|
104
|
+
self, raw: bytes | None = None, *, url: str | None = None, path: str | None = None
|
|
105
|
+
) -> None:
|
|
87
106
|
self.file_name: str = ""
|
|
88
107
|
if path:
|
|
89
108
|
self.file_name = Path(path).name
|
|
@@ -92,7 +111,7 @@ class Video(BaseFile):
|
|
|
92
111
|
|
|
93
112
|
if not self.file_name:
|
|
94
113
|
raise ValueError("Either url or path must be provided.")
|
|
95
|
-
super().__init__(url, path)
|
|
114
|
+
super().__init__(raw=raw, url=url, path=path)
|
|
96
115
|
|
|
97
116
|
@override
|
|
98
117
|
async def read(self) -> bytes:
|
|
@@ -100,7 +119,9 @@ class Video(BaseFile):
|
|
|
100
119
|
|
|
101
120
|
|
|
102
121
|
class File(BaseFile):
|
|
103
|
-
def __init__(
|
|
122
|
+
def __init__(
|
|
123
|
+
self, raw: bytes | None = None, *, url: str | None = None, path: str | None = None
|
|
124
|
+
) -> None:
|
|
104
125
|
self.file_name: str = ""
|
|
105
126
|
if path:
|
|
106
127
|
self.file_name = Path(path).name
|
|
@@ -110,7 +131,7 @@ class File(BaseFile):
|
|
|
110
131
|
if not self.file_name:
|
|
111
132
|
raise ValueError("Either url or path must be provided.")
|
|
112
133
|
|
|
113
|
-
super().__init__(url, path)
|
|
134
|
+
super().__init__(raw=raw, url=url, path=path)
|
|
114
135
|
|
|
115
136
|
@override
|
|
116
137
|
async def read(self) -> bytes:
|