maxapi-sdk 0.12.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.
- maxapi/__init__.py +49 -0
- maxapi/bot.py +674 -0
- maxapi/builders/__init__.py +23 -0
- maxapi/builders/keyboards.py +133 -0
- maxapi/builders/media.py +81 -0
- maxapi/callback_schema.py +140 -0
- maxapi/client/__init__.py +3 -0
- maxapi/client/default.py +89 -0
- maxapi/compat/__init__.py +15 -0
- maxapi/connection/__init__.py +3 -0
- maxapi/connection/base.py +136 -0
- maxapi/dispatcher.py +487 -0
- maxapi/exceptions/__init__.py +3 -0
- maxapi/exceptions/max.py +45 -0
- maxapi/filters/__init__.py +25 -0
- maxapi/filters/base.py +73 -0
- maxapi/filters/command.py +28 -0
- maxapi/filters/common.py +78 -0
- maxapi/filters/text.py +71 -0
- maxapi/fsm/__init__.py +17 -0
- maxapi/fsm/context.py +41 -0
- maxapi/fsm/filters.py +30 -0
- maxapi/fsm/middleware.py +42 -0
- maxapi/fsm/state.py +33 -0
- maxapi/fsm/storage/__init__.py +4 -0
- maxapi/fsm/storage/base.py +30 -0
- maxapi/fsm/storage/memory.py +38 -0
- maxapi/middlewares/__init__.py +3 -0
- maxapi/middlewares/base.py +34 -0
- maxapi/plugins/__init__.py +3 -0
- maxapi/plugins/base.py +19 -0
- maxapi/py.typed +1 -0
- maxapi/runners/__init__.py +4 -0
- maxapi/runners/polling.py +55 -0
- maxapi/runners/webhook.py +72 -0
- maxapi/transport/__init__.py +13 -0
- maxapi/transport/client.py +239 -0
- maxapi/transport/config.py +81 -0
- maxapi/transport/errors.py +45 -0
- maxapi/types/__init__.py +73 -0
- maxapi/types/base.py +9 -0
- maxapi/types/bot_mixin.py +14 -0
- maxapi/types/models.py +308 -0
- maxapi_sdk-0.12.0.dist-info/METADATA +270 -0
- maxapi_sdk-0.12.0.dist-info/RECORD +48 -0
- maxapi_sdk-0.12.0.dist-info/WHEEL +5 -0
- maxapi_sdk-0.12.0.dist-info/licenses/LICENSE +21 -0
- maxapi_sdk-0.12.0.dist-info/top_level.txt +1 -0
maxapi/bot.py
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Iterable
|
|
7
|
+
|
|
8
|
+
from .builders import build_uploaded_attachment, normalize_attachments
|
|
9
|
+
from .client.default import DefaultConnectionProperties
|
|
10
|
+
from .connection.base import BaseConnection
|
|
11
|
+
from .exceptions import InvalidToken, MaxApiError
|
|
12
|
+
from .types import (
|
|
13
|
+
AddAdminsRequest,
|
|
14
|
+
AddMembersRequest,
|
|
15
|
+
AnswerCallbackRequest,
|
|
16
|
+
Chat,
|
|
17
|
+
ChatsPage,
|
|
18
|
+
ChatAdmin,
|
|
19
|
+
EditMessageRequest,
|
|
20
|
+
EditMessageResponse,
|
|
21
|
+
MembersPage,
|
|
22
|
+
Message,
|
|
23
|
+
MessageBody,
|
|
24
|
+
MessageList,
|
|
25
|
+
PinMessageRequest,
|
|
26
|
+
SendMessageRequest,
|
|
27
|
+
SendMessageResponse,
|
|
28
|
+
SenderAction,
|
|
29
|
+
SubscriptionsPage,
|
|
30
|
+
SuccessResponse,
|
|
31
|
+
TextFormat,
|
|
32
|
+
UpdateType,
|
|
33
|
+
UpdatesPage,
|
|
34
|
+
UploadResponse,
|
|
35
|
+
UploadType,
|
|
36
|
+
User,
|
|
37
|
+
VideoInfo,
|
|
38
|
+
WebhookRequest,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Bot(BaseConnection):
|
|
43
|
+
"""Typed client для MAX Bot API."""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
token: str | None = None,
|
|
48
|
+
*,
|
|
49
|
+
default_connection: DefaultConnectionProperties | None = None,
|
|
50
|
+
api_url: str = "https://platform-api.max.ru",
|
|
51
|
+
marker_updates: int | None = None,
|
|
52
|
+
auto_check_subscriptions: bool = True,
|
|
53
|
+
) -> None:
|
|
54
|
+
super().__init__()
|
|
55
|
+
self.bot = self
|
|
56
|
+
self.default_connection = default_connection or DefaultConnectionProperties()
|
|
57
|
+
self.api_url = api_url.rstrip("/")
|
|
58
|
+
self.marker_updates = marker_updates
|
|
59
|
+
self.auto_check_subscriptions = auto_check_subscriptions
|
|
60
|
+
self.dispatcher = None
|
|
61
|
+
self._me: User | None = None
|
|
62
|
+
self.__token = token or os.environ.get("MAX_BOT_TOKEN")
|
|
63
|
+
if self.__token is None:
|
|
64
|
+
raise InvalidToken(
|
|
65
|
+
'Токен не может быть None. Укажите Bot(token="...") или MAX_BOT_TOKEN.'
|
|
66
|
+
)
|
|
67
|
+
self.headers: dict[str, str] = {"Authorization": self.__token}
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def me(self) -> User | None:
|
|
71
|
+
return self._me
|
|
72
|
+
|
|
73
|
+
async def close_session(self) -> None:
|
|
74
|
+
if self._transport is not None:
|
|
75
|
+
await self._transport.close()
|
|
76
|
+
elif self.session is not None and not self.session.closed:
|
|
77
|
+
await self.session.close()
|
|
78
|
+
|
|
79
|
+
async def get_me(self) -> User:
|
|
80
|
+
me = await self.request("GET", "/me", model=User)
|
|
81
|
+
self._me = me
|
|
82
|
+
return me
|
|
83
|
+
|
|
84
|
+
async def get_chats(self, *, count: int | None = None, marker: int | None = None) -> ChatsPage:
|
|
85
|
+
params = {"count": count, "marker": marker}
|
|
86
|
+
params = {key: value for key, value in params.items() if value is not None}
|
|
87
|
+
return await self.request("GET", "/chats", model=ChatsPage, params=params)
|
|
88
|
+
|
|
89
|
+
async def get_chat(self, chat_id: int) -> Chat:
|
|
90
|
+
return await self.request("GET", f"/chats/{chat_id}", model=Chat)
|
|
91
|
+
|
|
92
|
+
async def update_chat(
|
|
93
|
+
self,
|
|
94
|
+
chat_id: int,
|
|
95
|
+
*,
|
|
96
|
+
title: str | None = None,
|
|
97
|
+
icon: dict[str, Any] | None = None,
|
|
98
|
+
pin: str | None = None,
|
|
99
|
+
notify: bool | None = None,
|
|
100
|
+
) -> Chat:
|
|
101
|
+
payload = {
|
|
102
|
+
"title": title,
|
|
103
|
+
"icon": icon,
|
|
104
|
+
"pin": pin,
|
|
105
|
+
"notify": notify,
|
|
106
|
+
}
|
|
107
|
+
payload = {key: value for key, value in payload.items() if value is not None}
|
|
108
|
+
return await self.request("PATCH", f"/chats/{chat_id}", model=Chat, json=payload)
|
|
109
|
+
|
|
110
|
+
async def delete_chat(self, chat_id: int) -> SuccessResponse:
|
|
111
|
+
return await self.request("DELETE", f"/chats/{chat_id}", model=SuccessResponse)
|
|
112
|
+
|
|
113
|
+
async def send_chat_action(
|
|
114
|
+
self,
|
|
115
|
+
chat_id: int,
|
|
116
|
+
action: SenderAction = SenderAction.TYPING_ON,
|
|
117
|
+
) -> SuccessResponse:
|
|
118
|
+
return await self.request(
|
|
119
|
+
"POST",
|
|
120
|
+
f"/chats/{chat_id}/actions",
|
|
121
|
+
model=SuccessResponse,
|
|
122
|
+
json={"action": action.value},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
async def get_pinned_message(self, chat_id: int) -> SendMessageResponse:
|
|
126
|
+
return await self.request(
|
|
127
|
+
"GET",
|
|
128
|
+
f"/chats/{chat_id}/pin",
|
|
129
|
+
model=SendMessageResponse,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
async def pin_message(
|
|
133
|
+
self,
|
|
134
|
+
chat_id: int,
|
|
135
|
+
message_id: str,
|
|
136
|
+
*,
|
|
137
|
+
notify: bool | None = True,
|
|
138
|
+
) -> SuccessResponse:
|
|
139
|
+
payload = PinMessageRequest(message_id=message_id, notify=notify)
|
|
140
|
+
return await self.request(
|
|
141
|
+
"PUT",
|
|
142
|
+
f"/chats/{chat_id}/pin",
|
|
143
|
+
model=SuccessResponse,
|
|
144
|
+
json=payload.model_dump(by_alias=True, exclude_none=True),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
async def unpin_message(self, chat_id: int) -> SuccessResponse:
|
|
148
|
+
return await self.request(
|
|
149
|
+
"DELETE",
|
|
150
|
+
f"/chats/{chat_id}/pin",
|
|
151
|
+
model=SuccessResponse,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
async def get_membership(self, chat_id: int):
|
|
155
|
+
return await self.request(
|
|
156
|
+
"GET",
|
|
157
|
+
f"/chats/{chat_id}/members/me",
|
|
158
|
+
model=Chat,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
async def leave_chat(self, chat_id: int) -> SuccessResponse:
|
|
162
|
+
return await self.request(
|
|
163
|
+
"DELETE",
|
|
164
|
+
f"/chats/{chat_id}/members/me",
|
|
165
|
+
model=SuccessResponse,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
async def get_chat_admins(self, chat_id: int) -> MembersPage:
|
|
169
|
+
return await self.request(
|
|
170
|
+
"GET",
|
|
171
|
+
f"/chats/{chat_id}/members/admins",
|
|
172
|
+
model=MembersPage,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
async def add_chat_admins(self, chat_id: int, admins: list[ChatAdmin]) -> SuccessResponse:
|
|
176
|
+
payload = AddAdminsRequest(admins=admins)
|
|
177
|
+
return await self.request(
|
|
178
|
+
"POST",
|
|
179
|
+
f"/chats/{chat_id}/members/admins",
|
|
180
|
+
model=SuccessResponse,
|
|
181
|
+
json=payload.model_dump(by_alias=True, exclude_none=True),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
async def remove_chat_admin(self, chat_id: int, user_id: int) -> SuccessResponse:
|
|
185
|
+
return await self.request(
|
|
186
|
+
"DELETE",
|
|
187
|
+
f"/chats/{chat_id}/members/admins/{user_id}",
|
|
188
|
+
model=SuccessResponse,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
async def get_chat_members(
|
|
192
|
+
self,
|
|
193
|
+
chat_id: int,
|
|
194
|
+
*,
|
|
195
|
+
user_ids: list[int] | None = None,
|
|
196
|
+
marker: int | None = None,
|
|
197
|
+
count: int | None = None,
|
|
198
|
+
) -> MembersPage:
|
|
199
|
+
params = {"marker": marker, "count": count}
|
|
200
|
+
if user_ids:
|
|
201
|
+
params["user_ids"] = ",".join(str(value) for value in user_ids)
|
|
202
|
+
params = {key: value for key, value in params.items() if value is not None}
|
|
203
|
+
return await self.request(
|
|
204
|
+
"GET",
|
|
205
|
+
f"/chats/{chat_id}/members",
|
|
206
|
+
model=MembersPage,
|
|
207
|
+
params=params,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
async def add_chat_members(self, chat_id: int, user_ids: list[int]) -> SuccessResponse:
|
|
211
|
+
payload = AddMembersRequest(user_ids=user_ids)
|
|
212
|
+
return await self.request(
|
|
213
|
+
"POST",
|
|
214
|
+
f"/chats/{chat_id}/members",
|
|
215
|
+
model=SuccessResponse,
|
|
216
|
+
json=payload.model_dump(by_alias=True, exclude_none=True),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
async def remove_chat_member(
|
|
220
|
+
self,
|
|
221
|
+
chat_id: int,
|
|
222
|
+
user_id: int,
|
|
223
|
+
*,
|
|
224
|
+
block: bool | None = None,
|
|
225
|
+
) -> SuccessResponse:
|
|
226
|
+
params = {"user_id": user_id, "block": block}
|
|
227
|
+
params = {key: value for key, value in params.items() if value is not None}
|
|
228
|
+
return await self.request(
|
|
229
|
+
"DELETE",
|
|
230
|
+
f"/chats/{chat_id}/members",
|
|
231
|
+
model=SuccessResponse,
|
|
232
|
+
params=params,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
async def get_subscriptions(self) -> SubscriptionsPage:
|
|
236
|
+
return await self.request("GET", "/subscriptions", model=SubscriptionsPage)
|
|
237
|
+
|
|
238
|
+
async def set_webhook(
|
|
239
|
+
self,
|
|
240
|
+
url: str,
|
|
241
|
+
*,
|
|
242
|
+
update_types: Iterable[UpdateType | str] | None = None,
|
|
243
|
+
secret: str | None = None,
|
|
244
|
+
) -> SuccessResponse:
|
|
245
|
+
normalized_types = None
|
|
246
|
+
if update_types is not None:
|
|
247
|
+
normalized_types = [item.value if hasattr(item, "value") else item for item in update_types]
|
|
248
|
+
payload = WebhookRequest(url=url, update_types=normalized_types, secret=secret)
|
|
249
|
+
return await self.request(
|
|
250
|
+
"POST",
|
|
251
|
+
"/subscriptions",
|
|
252
|
+
model=SuccessResponse,
|
|
253
|
+
json=payload.model_dump(by_alias=True, exclude_none=True),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
async def delete_webhook(self, url: str | None = None) -> SuccessResponse:
|
|
257
|
+
params = {"url": url} if url is not None else None
|
|
258
|
+
return await self.request(
|
|
259
|
+
"DELETE",
|
|
260
|
+
"/subscriptions",
|
|
261
|
+
model=SuccessResponse,
|
|
262
|
+
params=params,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
async def get_updates(
|
|
266
|
+
self,
|
|
267
|
+
*,
|
|
268
|
+
marker: int | None = None,
|
|
269
|
+
limit: int = 100,
|
|
270
|
+
timeout: int = 30,
|
|
271
|
+
types: Iterable[UpdateType | str] | None = None,
|
|
272
|
+
) -> UpdatesPage:
|
|
273
|
+
params: dict[str, Any] = {"limit": limit, "timeout": timeout}
|
|
274
|
+
if marker is not None:
|
|
275
|
+
params["marker"] = marker
|
|
276
|
+
if types:
|
|
277
|
+
params["types"] = ",".join(item.value if hasattr(item, "value") else item for item in types)
|
|
278
|
+
return await self.request("GET", "/updates", model=UpdatesPage, params=params)
|
|
279
|
+
|
|
280
|
+
async def create_upload(self, upload_type: UploadType | str) -> UploadResponse:
|
|
281
|
+
upload_value = upload_type.value if hasattr(upload_type, "value") else upload_type
|
|
282
|
+
return await self.request(
|
|
283
|
+
"POST",
|
|
284
|
+
"/uploads",
|
|
285
|
+
model=UploadResponse,
|
|
286
|
+
params={"type": upload_value},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
async def get_messages(
|
|
290
|
+
self,
|
|
291
|
+
*,
|
|
292
|
+
chat_id: int | None = None,
|
|
293
|
+
message_ids: list[str] | None = None,
|
|
294
|
+
from_time: int | None = None,
|
|
295
|
+
to_time: int | None = None,
|
|
296
|
+
count: int | None = None,
|
|
297
|
+
) -> MessageList:
|
|
298
|
+
params: dict[str, Any] = {}
|
|
299
|
+
if chat_id is not None:
|
|
300
|
+
params["chat_id"] = chat_id
|
|
301
|
+
if message_ids is not None:
|
|
302
|
+
params["message_ids"] = ",".join(message_ids)
|
|
303
|
+
if from_time is not None:
|
|
304
|
+
params["from_time"] = from_time
|
|
305
|
+
if to_time is not None:
|
|
306
|
+
params["to_time"] = to_time
|
|
307
|
+
if count is not None:
|
|
308
|
+
params["count"] = count
|
|
309
|
+
return await self.request("GET", "/messages", model=MessageList, params=params)
|
|
310
|
+
|
|
311
|
+
async def get_message(self, message_id: str) -> Message:
|
|
312
|
+
return await self.request("GET", f"/messages/{message_id}", model=Message)
|
|
313
|
+
|
|
314
|
+
async def send_message(
|
|
315
|
+
self,
|
|
316
|
+
*,
|
|
317
|
+
chat_id: int | None = None,
|
|
318
|
+
user_id: int | None = None,
|
|
319
|
+
text: str | None = None,
|
|
320
|
+
attachments: list[dict[str, Any]] | None = None,
|
|
321
|
+
keyboard: Any | None = None,
|
|
322
|
+
link: dict[str, Any] | None = None,
|
|
323
|
+
format: TextFormat | None = None,
|
|
324
|
+
notify: bool | None = None,
|
|
325
|
+
disable_link_preview: bool | None = None,
|
|
326
|
+
) -> SendMessageResponse:
|
|
327
|
+
params = {
|
|
328
|
+
"chat_id": chat_id,
|
|
329
|
+
"user_id": user_id,
|
|
330
|
+
"disable_link_preview": disable_link_preview,
|
|
331
|
+
}
|
|
332
|
+
params = {key: value for key, value in params.items() if value is not None}
|
|
333
|
+
payload = SendMessageRequest(
|
|
334
|
+
text=text,
|
|
335
|
+
attachments=normalize_attachments(attachments, keyboard=keyboard),
|
|
336
|
+
link=link,
|
|
337
|
+
notify=notify,
|
|
338
|
+
format=format,
|
|
339
|
+
)
|
|
340
|
+
return await self.request(
|
|
341
|
+
"POST",
|
|
342
|
+
"/messages",
|
|
343
|
+
model=SendMessageResponse,
|
|
344
|
+
params=params,
|
|
345
|
+
json=payload.model_dump(by_alias=True, exclude_none=True),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
async def edit_message(
|
|
349
|
+
self,
|
|
350
|
+
*,
|
|
351
|
+
message_id: str,
|
|
352
|
+
text: str | None = None,
|
|
353
|
+
attachments: list[dict[str, Any]] | None = None,
|
|
354
|
+
keyboard: Any | None = None,
|
|
355
|
+
link: dict[str, Any] | None = None,
|
|
356
|
+
format: TextFormat | None = None,
|
|
357
|
+
notify: bool | None = None,
|
|
358
|
+
) -> EditMessageResponse:
|
|
359
|
+
payload = EditMessageRequest(
|
|
360
|
+
message_id=message_id,
|
|
361
|
+
text=text,
|
|
362
|
+
attachments=normalize_attachments(attachments, keyboard=keyboard),
|
|
363
|
+
link=link,
|
|
364
|
+
notify=notify,
|
|
365
|
+
format=format,
|
|
366
|
+
)
|
|
367
|
+
return await self.request(
|
|
368
|
+
"PUT",
|
|
369
|
+
"/messages",
|
|
370
|
+
model=EditMessageResponse,
|
|
371
|
+
json=payload.model_dump(by_alias=True, exclude_none=True),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
async def delete_message(self, message_id: str) -> SuccessResponse:
|
|
375
|
+
return await self.request(
|
|
376
|
+
"DELETE",
|
|
377
|
+
"/messages",
|
|
378
|
+
model=SuccessResponse,
|
|
379
|
+
params={"message_id": message_id},
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
async def get_video_info(self, message_id: str) -> VideoInfo:
|
|
383
|
+
return await self.request(
|
|
384
|
+
"GET",
|
|
385
|
+
f"/messages/{message_id}/video",
|
|
386
|
+
model=VideoInfo,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
async def answer_callback(
|
|
390
|
+
self,
|
|
391
|
+
callback_id: str,
|
|
392
|
+
*,
|
|
393
|
+
notification: str | None = None,
|
|
394
|
+
message: MessageBody | None = None,
|
|
395
|
+
keyboard: Any | None = None,
|
|
396
|
+
) -> SuccessResponse:
|
|
397
|
+
payload_message = message
|
|
398
|
+
if payload_message is not None:
|
|
399
|
+
payload_message = payload_message.model_copy(
|
|
400
|
+
update={
|
|
401
|
+
"attachments": normalize_attachments(
|
|
402
|
+
payload_message.attachments,
|
|
403
|
+
keyboard=keyboard,
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
)
|
|
407
|
+
elif keyboard is not None:
|
|
408
|
+
payload_message = MessageBody(attachments=normalize_attachments(keyboard=keyboard))
|
|
409
|
+
payload = AnswerCallbackRequest(notification=notification, message=payload_message)
|
|
410
|
+
return await self.request(
|
|
411
|
+
"POST",
|
|
412
|
+
"/answers",
|
|
413
|
+
model=SuccessResponse,
|
|
414
|
+
params={"callback_id": callback_id},
|
|
415
|
+
json=payload.model_dump(by_alias=True, exclude_none=True),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
async def upload_attachment(
|
|
419
|
+
self,
|
|
420
|
+
*,
|
|
421
|
+
upload_type: UploadType | str,
|
|
422
|
+
path: str | os.PathLike[str] | None = None,
|
|
423
|
+
filename: str | None = None,
|
|
424
|
+
buffer: bytes | None = None,
|
|
425
|
+
) -> dict[str, Any]:
|
|
426
|
+
upload_response = await self.create_upload(upload_type)
|
|
427
|
+
upload_value = upload_type.value if hasattr(upload_type, "value") else str(upload_type)
|
|
428
|
+
if path is None and buffer is None:
|
|
429
|
+
raise ValueError("Необходимо передать path или buffer для upload_attachment.")
|
|
430
|
+
if path is not None:
|
|
431
|
+
uploaded_payload = await self.upload_file(upload_response.url, str(path), upload_value)
|
|
432
|
+
else:
|
|
433
|
+
if filename is None:
|
|
434
|
+
raise ValueError("Для buffer-загрузки необходимо передать filename.")
|
|
435
|
+
uploaded_payload = await self.upload_file_buffer(
|
|
436
|
+
filename=filename,
|
|
437
|
+
url=upload_response.url,
|
|
438
|
+
buffer=buffer if buffer is not None else b"",
|
|
439
|
+
upload_type=upload_value,
|
|
440
|
+
)
|
|
441
|
+
if not isinstance(uploaded_payload, dict):
|
|
442
|
+
raise TypeError("MAX upload вернул неожиданный payload; ожидался JSON-объект.")
|
|
443
|
+
return build_uploaded_attachment(
|
|
444
|
+
upload_type=upload_value,
|
|
445
|
+
upload_response_token=upload_response.token,
|
|
446
|
+
uploaded_payload=uploaded_payload,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
async def upload_image(self, path: str | os.PathLike[str]) -> dict[str, Any]:
|
|
450
|
+
return await self.upload_attachment(upload_type=UploadType.IMAGE, path=path)
|
|
451
|
+
|
|
452
|
+
async def upload_video(self, path: str | os.PathLike[str]) -> dict[str, Any]:
|
|
453
|
+
return await self.upload_attachment(upload_type=UploadType.VIDEO, path=path)
|
|
454
|
+
|
|
455
|
+
async def upload_audio(self, path: str | os.PathLike[str]) -> dict[str, Any]:
|
|
456
|
+
return await self.upload_attachment(upload_type=UploadType.AUDIO, path=path)
|
|
457
|
+
|
|
458
|
+
async def upload_file_attachment(self, path: str | os.PathLike[str]) -> dict[str, Any]:
|
|
459
|
+
return await self.upload_attachment(upload_type=UploadType.FILE, path=path)
|
|
460
|
+
|
|
461
|
+
async def send_media(
|
|
462
|
+
self,
|
|
463
|
+
*,
|
|
464
|
+
upload_type: UploadType | str,
|
|
465
|
+
path: str | os.PathLike[str],
|
|
466
|
+
chat_id: int | None = None,
|
|
467
|
+
user_id: int | None = None,
|
|
468
|
+
text: str | None = None,
|
|
469
|
+
keyboard: Any | None = None,
|
|
470
|
+
format: TextFormat | None = None,
|
|
471
|
+
notify: bool | None = None,
|
|
472
|
+
disable_link_preview: bool | None = None,
|
|
473
|
+
processing_wait: float = 0.0,
|
|
474
|
+
attachment_ready_retries: int = 3,
|
|
475
|
+
attachment_ready_delay: float = 1.0,
|
|
476
|
+
attachment_ready_backoff: float = 2.0,
|
|
477
|
+
) -> SendMessageResponse:
|
|
478
|
+
attachment = await self.upload_attachment(upload_type=upload_type, path=path)
|
|
479
|
+
if processing_wait > 0:
|
|
480
|
+
await asyncio.sleep(processing_wait)
|
|
481
|
+
return await self._send_with_attachment_retry(
|
|
482
|
+
chat_id=chat_id,
|
|
483
|
+
user_id=user_id,
|
|
484
|
+
text=text,
|
|
485
|
+
attachments=[attachment],
|
|
486
|
+
keyboard=keyboard,
|
|
487
|
+
format=format,
|
|
488
|
+
notify=notify,
|
|
489
|
+
disable_link_preview=disable_link_preview,
|
|
490
|
+
retries=attachment_ready_retries,
|
|
491
|
+
retry_delay=attachment_ready_delay,
|
|
492
|
+
retry_backoff=attachment_ready_backoff,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
async def send_image(
|
|
496
|
+
self,
|
|
497
|
+
path: str | os.PathLike[str],
|
|
498
|
+
*,
|
|
499
|
+
chat_id: int | None = None,
|
|
500
|
+
user_id: int | None = None,
|
|
501
|
+
text: str | None = None,
|
|
502
|
+
keyboard: Any | None = None,
|
|
503
|
+
format: TextFormat | None = None,
|
|
504
|
+
notify: bool | None = None,
|
|
505
|
+
disable_link_preview: bool | None = None,
|
|
506
|
+
processing_wait: float = 0.0,
|
|
507
|
+
attachment_ready_retries: int = 3,
|
|
508
|
+
attachment_ready_delay: float = 1.0,
|
|
509
|
+
attachment_ready_backoff: float = 2.0,
|
|
510
|
+
) -> SendMessageResponse:
|
|
511
|
+
return await self.send_media(
|
|
512
|
+
upload_type=UploadType.IMAGE,
|
|
513
|
+
path=path,
|
|
514
|
+
chat_id=chat_id,
|
|
515
|
+
user_id=user_id,
|
|
516
|
+
text=text,
|
|
517
|
+
keyboard=keyboard,
|
|
518
|
+
format=format,
|
|
519
|
+
notify=notify,
|
|
520
|
+
disable_link_preview=disable_link_preview,
|
|
521
|
+
processing_wait=processing_wait,
|
|
522
|
+
attachment_ready_retries=attachment_ready_retries,
|
|
523
|
+
attachment_ready_delay=attachment_ready_delay,
|
|
524
|
+
attachment_ready_backoff=attachment_ready_backoff,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
async def send_video(
|
|
528
|
+
self,
|
|
529
|
+
path: str | os.PathLike[str],
|
|
530
|
+
*,
|
|
531
|
+
chat_id: int | None = None,
|
|
532
|
+
user_id: int | None = None,
|
|
533
|
+
text: str | None = None,
|
|
534
|
+
keyboard: Any | None = None,
|
|
535
|
+
format: TextFormat | None = None,
|
|
536
|
+
notify: bool | None = None,
|
|
537
|
+
disable_link_preview: bool | None = None,
|
|
538
|
+
processing_wait: float = 0.0,
|
|
539
|
+
attachment_ready_retries: int = 3,
|
|
540
|
+
attachment_ready_delay: float = 1.0,
|
|
541
|
+
attachment_ready_backoff: float = 2.0,
|
|
542
|
+
) -> SendMessageResponse:
|
|
543
|
+
return await self.send_media(
|
|
544
|
+
upload_type=UploadType.VIDEO,
|
|
545
|
+
path=path,
|
|
546
|
+
chat_id=chat_id,
|
|
547
|
+
user_id=user_id,
|
|
548
|
+
text=text,
|
|
549
|
+
keyboard=keyboard,
|
|
550
|
+
format=format,
|
|
551
|
+
notify=notify,
|
|
552
|
+
disable_link_preview=disable_link_preview,
|
|
553
|
+
processing_wait=processing_wait,
|
|
554
|
+
attachment_ready_retries=attachment_ready_retries,
|
|
555
|
+
attachment_ready_delay=attachment_ready_delay,
|
|
556
|
+
attachment_ready_backoff=attachment_ready_backoff,
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
async def send_audio(
|
|
560
|
+
self,
|
|
561
|
+
path: str | os.PathLike[str],
|
|
562
|
+
*,
|
|
563
|
+
chat_id: int | None = None,
|
|
564
|
+
user_id: int | None = None,
|
|
565
|
+
text: str | None = None,
|
|
566
|
+
keyboard: Any | None = None,
|
|
567
|
+
format: TextFormat | None = None,
|
|
568
|
+
notify: bool | None = None,
|
|
569
|
+
disable_link_preview: bool | None = None,
|
|
570
|
+
processing_wait: float = 0.0,
|
|
571
|
+
attachment_ready_retries: int = 3,
|
|
572
|
+
attachment_ready_delay: float = 1.0,
|
|
573
|
+
attachment_ready_backoff: float = 2.0,
|
|
574
|
+
) -> SendMessageResponse:
|
|
575
|
+
return await self.send_media(
|
|
576
|
+
upload_type=UploadType.AUDIO,
|
|
577
|
+
path=path,
|
|
578
|
+
chat_id=chat_id,
|
|
579
|
+
user_id=user_id,
|
|
580
|
+
text=text,
|
|
581
|
+
keyboard=keyboard,
|
|
582
|
+
format=format,
|
|
583
|
+
notify=notify,
|
|
584
|
+
disable_link_preview=disable_link_preview,
|
|
585
|
+
processing_wait=processing_wait,
|
|
586
|
+
attachment_ready_retries=attachment_ready_retries,
|
|
587
|
+
attachment_ready_delay=attachment_ready_delay,
|
|
588
|
+
attachment_ready_backoff=attachment_ready_backoff,
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
async def send_file(
|
|
592
|
+
self,
|
|
593
|
+
path: str | os.PathLike[str],
|
|
594
|
+
*,
|
|
595
|
+
chat_id: int | None = None,
|
|
596
|
+
user_id: int | None = None,
|
|
597
|
+
text: str | None = None,
|
|
598
|
+
keyboard: Any | None = None,
|
|
599
|
+
format: TextFormat | None = None,
|
|
600
|
+
notify: bool | None = None,
|
|
601
|
+
disable_link_preview: bool | None = None,
|
|
602
|
+
processing_wait: float = 0.0,
|
|
603
|
+
attachment_ready_retries: int = 3,
|
|
604
|
+
attachment_ready_delay: float = 1.0,
|
|
605
|
+
attachment_ready_backoff: float = 2.0,
|
|
606
|
+
) -> SendMessageResponse:
|
|
607
|
+
return await self.send_media(
|
|
608
|
+
upload_type=UploadType.FILE,
|
|
609
|
+
path=path,
|
|
610
|
+
chat_id=chat_id,
|
|
611
|
+
user_id=user_id,
|
|
612
|
+
text=text,
|
|
613
|
+
keyboard=keyboard,
|
|
614
|
+
format=format,
|
|
615
|
+
notify=notify,
|
|
616
|
+
disable_link_preview=disable_link_preview,
|
|
617
|
+
processing_wait=processing_wait,
|
|
618
|
+
attachment_ready_retries=attachment_ready_retries,
|
|
619
|
+
attachment_ready_delay=attachment_ready_delay,
|
|
620
|
+
attachment_ready_backoff=attachment_ready_backoff,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
async def send_text(self, *args: Any, **kwargs: Any) -> SendMessageResponse:
|
|
624
|
+
return await self.send_message(*args, **kwargs)
|
|
625
|
+
|
|
626
|
+
async def edit_text(self, *args: Any, **kwargs: Any) -> EditMessageResponse:
|
|
627
|
+
return await self.edit_message(*args, **kwargs)
|
|
628
|
+
|
|
629
|
+
async def delete(self, message_id: str) -> SuccessResponse:
|
|
630
|
+
return await self.delete_message(message_id)
|
|
631
|
+
|
|
632
|
+
async def answer_callback_query(self, callback_id: str, **kwargs: Any) -> SuccessResponse:
|
|
633
|
+
return await self.answer_callback(callback_id, **kwargs)
|
|
634
|
+
|
|
635
|
+
async def _send_with_attachment_retry(
|
|
636
|
+
self,
|
|
637
|
+
*,
|
|
638
|
+
chat_id: int | None,
|
|
639
|
+
user_id: int | None,
|
|
640
|
+
text: str | None,
|
|
641
|
+
attachments: list[dict[str, Any]] | None,
|
|
642
|
+
keyboard: Any | None,
|
|
643
|
+
format: TextFormat | None,
|
|
644
|
+
notify: bool | None,
|
|
645
|
+
disable_link_preview: bool | None,
|
|
646
|
+
retries: int,
|
|
647
|
+
retry_delay: float,
|
|
648
|
+
retry_backoff: float,
|
|
649
|
+
) -> SendMessageResponse:
|
|
650
|
+
attempt = 0
|
|
651
|
+
current_delay = retry_delay
|
|
652
|
+
while True:
|
|
653
|
+
try:
|
|
654
|
+
return await self.send_message(
|
|
655
|
+
chat_id=chat_id,
|
|
656
|
+
user_id=user_id,
|
|
657
|
+
text=text,
|
|
658
|
+
attachments=attachments,
|
|
659
|
+
keyboard=keyboard,
|
|
660
|
+
format=format,
|
|
661
|
+
notify=notify,
|
|
662
|
+
disable_link_preview=disable_link_preview,
|
|
663
|
+
)
|
|
664
|
+
except MaxApiError as exc:
|
|
665
|
+
if not self._is_attachment_not_ready(exc) or attempt >= retries:
|
|
666
|
+
raise
|
|
667
|
+
attempt += 1
|
|
668
|
+
await asyncio.sleep(current_delay)
|
|
669
|
+
current_delay *= max(retry_backoff, 1.0)
|
|
670
|
+
|
|
671
|
+
@staticmethod
|
|
672
|
+
def _is_attachment_not_ready(exc: MaxApiError) -> bool:
|
|
673
|
+
raw = exc.raw if isinstance(exc.raw, dict) else {}
|
|
674
|
+
return raw.get("code") == "attachment.not.ready"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .keyboards import InlineKeyboardBuilder
|
|
2
|
+
from .media import (
|
|
3
|
+
audio_attachment,
|
|
4
|
+
build_uploaded_attachment,
|
|
5
|
+
file_attachment,
|
|
6
|
+
image_attachment,
|
|
7
|
+
make_attachment,
|
|
8
|
+
normalize_attachment,
|
|
9
|
+
normalize_attachments,
|
|
10
|
+
video_attachment,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"InlineKeyboardBuilder",
|
|
15
|
+
"audio_attachment",
|
|
16
|
+
"build_uploaded_attachment",
|
|
17
|
+
"file_attachment",
|
|
18
|
+
"image_attachment",
|
|
19
|
+
"make_attachment",
|
|
20
|
+
"normalize_attachment",
|
|
21
|
+
"normalize_attachments",
|
|
22
|
+
"video_attachment",
|
|
23
|
+
]
|