maxibot 0.98.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maxibot/__init__.py +644 -0
- maxibot/apihelper.py +241 -0
- maxibot/core/attachments/photo.py +45 -0
- maxibot/core/network/client.py +118 -0
- maxibot/core/network/polling.py +75 -0
- maxibot/types.py +753 -0
- maxibot/util.py +98 -0
- maxibot-0.98.1.dist-info/METADATA +56 -0
- maxibot-0.98.1.dist-info/RECORD +11 -0
- maxibot-0.98.1.dist-info/WHEEL +5 -0
- maxibot-0.98.1.dist-info/top_level.txt +1 -0
maxibot/types.py
ADDED
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import List, Dict, Any, Optional
|
|
4
|
+
|
|
5
|
+
from maxibot.apihelper import Api
|
|
6
|
+
from maxibot.util import is_pil_image, pil_image_to_bytes
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UpdateType:
|
|
10
|
+
"""
|
|
11
|
+
Типы обновлений, которые можно получать от MAX API
|
|
12
|
+
"""
|
|
13
|
+
MESSAGE_CREATED = "message_created"
|
|
14
|
+
MESSAGE_CALLBACK = "message_callback"
|
|
15
|
+
BOT_STARTED = "bot_started"
|
|
16
|
+
MESSAGE_EDITED = "message_edited"
|
|
17
|
+
MESSAGE_DELETED = "message_deleted"
|
|
18
|
+
MESSAGE_CHAT_CREATED = "message_chat_created"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class InlineKeyboardButton:
|
|
22
|
+
"""
|
|
23
|
+
Класс для создания inline-кнопок в сообщениях
|
|
24
|
+
|
|
25
|
+
:param text: Текст на кнопке
|
|
26
|
+
:type text: str
|
|
27
|
+
|
|
28
|
+
:param url: URL ссылка для кнопки типа "link"
|
|
29
|
+
:type url: Optional[str]
|
|
30
|
+
|
|
31
|
+
:param callback_data: Данные для callback-кнопки
|
|
32
|
+
:type callback_data: Optional[str]
|
|
33
|
+
"""
|
|
34
|
+
MAX_URL_LEN = 2048
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
text: str,
|
|
39
|
+
url: Optional[str] = None,
|
|
40
|
+
callback_data: Optional[str] = None,
|
|
41
|
+
):
|
|
42
|
+
self.text = text
|
|
43
|
+
self.url = url
|
|
44
|
+
self.callback_data = callback_data
|
|
45
|
+
|
|
46
|
+
if not (url or callback_data):
|
|
47
|
+
raise ValueError("url или callback_data обязан быть")
|
|
48
|
+
if url and callback_data:
|
|
49
|
+
raise ValueError("укажите что-то одно")
|
|
50
|
+
if url and len(url) > self.MAX_URL_LEN:
|
|
51
|
+
raise ValueError(f"url не может быть длиннее {self.MAX_URL_LEN} символов")
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
54
|
+
"""
|
|
55
|
+
Преобразует кнопку в словарь для отправки в MAX API
|
|
56
|
+
|
|
57
|
+
:return: Словарь с данными кнопки в формате MAX API
|
|
58
|
+
:rtype: Dict[str, Any]
|
|
59
|
+
"""
|
|
60
|
+
if self.url:
|
|
61
|
+
return {"type": "link", "text": self.text, "url": self.url}
|
|
62
|
+
return {
|
|
63
|
+
"type": "callback",
|
|
64
|
+
"text": self.text,
|
|
65
|
+
"payload": self.callback_data
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
def is_special(self) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Проверяет, является ли кнопка специальной (ограничивает ряд до 3 кнопок)
|
|
71
|
+
|
|
72
|
+
:return: True если кнопка специальная (link), False если обычная (callback)
|
|
73
|
+
:rtype: bool
|
|
74
|
+
"""
|
|
75
|
+
return self.url is not None # link
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class InlineKeyboardMarkup:
|
|
79
|
+
"""
|
|
80
|
+
Класс для создания inline-клавиатур в сообщениях
|
|
81
|
+
|
|
82
|
+
:param row_width: Ширина ряда по умолчанию (сколько кнопок в ряду)
|
|
83
|
+
:type row_width: int
|
|
84
|
+
"""
|
|
85
|
+
MAX_ROWS = 30
|
|
86
|
+
MAX_BUTTONS = 210
|
|
87
|
+
MAX_ROW_REGULAR = 7
|
|
88
|
+
MAX_ROW_SPECIAL = 3
|
|
89
|
+
|
|
90
|
+
def __init__(self, row_width: int = 1):
|
|
91
|
+
self.row_width = row_width
|
|
92
|
+
self.keyboard: List[List[InlineKeyboardButton]] = []
|
|
93
|
+
|
|
94
|
+
def add(self, *args: InlineKeyboardButton, row_width=None) -> 'InlineKeyboardMarkup':
|
|
95
|
+
"""
|
|
96
|
+
Добавляет кнопки в клавиатуру, автоматически разбивая на ряды
|
|
97
|
+
|
|
98
|
+
:param args: Кнопки для добавления
|
|
99
|
+
:type args: InlineKeyboardButton
|
|
100
|
+
|
|
101
|
+
:param row_width: Ширина ряда для этих кнопок (если не указано, используется self.row_width)
|
|
102
|
+
:type row_width: Optional[int]
|
|
103
|
+
|
|
104
|
+
:return: Текущий объект клавиатуры (для цепочки вызовов)
|
|
105
|
+
:rtype: InlineKeyboardMarkup
|
|
106
|
+
"""
|
|
107
|
+
width = row_width or self.row_width
|
|
108
|
+
row = []
|
|
109
|
+
for btn in args:
|
|
110
|
+
row.append(btn)
|
|
111
|
+
if len(row) == width:
|
|
112
|
+
self._append_row(row)
|
|
113
|
+
row = []
|
|
114
|
+
if row:
|
|
115
|
+
self._append_row(row)
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def row(self, *args: InlineKeyboardButton) -> 'InlineKeyboardMarkup':
|
|
119
|
+
"""
|
|
120
|
+
Добавляет ряд кнопок в клавиатуру
|
|
121
|
+
|
|
122
|
+
:param args: Кнопки для добавления в ряд
|
|
123
|
+
:type args: InlineKeyboardButton
|
|
124
|
+
|
|
125
|
+
:return: Текущий объект клавиатуры (для цепочки вызовов)
|
|
126
|
+
:rtype: InlineKeyboardMarkup
|
|
127
|
+
"""
|
|
128
|
+
if args:
|
|
129
|
+
self._append_row(list(args))
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def to_attachment(self) -> Dict[str, Any]:
|
|
133
|
+
"""
|
|
134
|
+
Преобразует клавиатуру в attachment для отправки в сообщении
|
|
135
|
+
|
|
136
|
+
:return: Словарь с данными клавиатуры в формате MAX API
|
|
137
|
+
:rtype: Dict[str, Any]
|
|
138
|
+
"""
|
|
139
|
+
self._validate()
|
|
140
|
+
return {
|
|
141
|
+
"type": "inline_keyboard",
|
|
142
|
+
"payload": {"buttons": [[btn.to_dict() for btn in row] for row in self.keyboard]},
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
def _append_row(self, row: List[InlineKeyboardButton]):
|
|
146
|
+
"""
|
|
147
|
+
Метод для добавления ряда кнопок
|
|
148
|
+
|
|
149
|
+
:param row: Ряд кнопок для добавления
|
|
150
|
+
:type row: List[InlineKeyboardButton]
|
|
151
|
+
"""
|
|
152
|
+
self.keyboard.append(row)
|
|
153
|
+
|
|
154
|
+
def _validate(self):
|
|
155
|
+
"""
|
|
156
|
+
Метод для валидации клавиатуры
|
|
157
|
+
|
|
158
|
+
:raises ValueError: Если превышены лимиты на количество кнопок или рядов
|
|
159
|
+
"""
|
|
160
|
+
total = sum(len(r) for r in self.keyboard)
|
|
161
|
+
if total > self.MAX_BUTTONS:
|
|
162
|
+
raise ValueError(f"Максимум {self.MAX_BUTTONS} кнопок")
|
|
163
|
+
if len(self.keyboard) > self.MAX_ROWS:
|
|
164
|
+
raise ValueError(f"Максимум {self.MAX_ROWS} рядов")
|
|
165
|
+
|
|
166
|
+
for row in self.keyboard:
|
|
167
|
+
special_in_row = any(btn.is_special() for btn in row)
|
|
168
|
+
limit = self.MAX_ROW_SPECIAL if special_in_row else self.MAX_ROW_REGULAR
|
|
169
|
+
if len(row) > limit:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
f"Ряд содержит {len(row)} кнопок, но максимум {limit} "
|
|
172
|
+
f"(из-за special-кнопок)" if special_in_row else ""
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class ImagePayload:
|
|
177
|
+
"""
|
|
178
|
+
Класс для хранения данных изображения
|
|
179
|
+
|
|
180
|
+
:param payload: Словарь с данными изображения
|
|
181
|
+
:type payload: Dict[str, Any]
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def __init__(self, payload: Dict[str, Any]):
|
|
185
|
+
self.photo_id = payload.get("photo_id")
|
|
186
|
+
self.token = payload.get("token")
|
|
187
|
+
self.url = payload.get("url")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class ImageAttachment:
|
|
191
|
+
"""
|
|
192
|
+
Класс для работы с вложениями типа "image"
|
|
193
|
+
|
|
194
|
+
:param attach: Словарь с данными вложения
|
|
195
|
+
:type attach: Dict[str, Any]
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def __init__(self, attach: Dict[str, Any]):
|
|
199
|
+
self.payload = ImagePayload(payload=attach.get("payload"))
|
|
200
|
+
self.type = attach.get("type")
|
|
201
|
+
|
|
202
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
203
|
+
"""
|
|
204
|
+
Преобразует объект в словарь
|
|
205
|
+
|
|
206
|
+
:return: Словарь с данными изображения
|
|
207
|
+
:rtype: Dict[str, Any]
|
|
208
|
+
"""
|
|
209
|
+
return {
|
|
210
|
+
"payload": {
|
|
211
|
+
"photo_id": self.payload.photo_id,
|
|
212
|
+
"token": self.payload.token,
|
|
213
|
+
"url": self.payload.url
|
|
214
|
+
},
|
|
215
|
+
"type": self.type
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class Recipient:
|
|
220
|
+
"""
|
|
221
|
+
Класс получателя сообщения
|
|
222
|
+
|
|
223
|
+
:param rec: Словарь recipient из ответа MAX API
|
|
224
|
+
:type rec: Dict[str, Any]
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
def __init__(self, rec: Dict[str, Any]):
|
|
228
|
+
self.chat_id = rec.get("chat_id")
|
|
229
|
+
self.chat_type = rec.get("chat_type")
|
|
230
|
+
self.user_id = rec.get("user_id")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class Body:
|
|
234
|
+
"""
|
|
235
|
+
Класс тела сообщения
|
|
236
|
+
|
|
237
|
+
:param body: Словарь body из ответа MAX API
|
|
238
|
+
:type body: Dict[str, Any]
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
def __init__(self, body: Dict[str, Any]):
|
|
242
|
+
self.mid = body.get("mid")
|
|
243
|
+
self.seq = body.get("seq")
|
|
244
|
+
self.text = body.get("text")
|
|
245
|
+
self.attachments = body.get("attachments")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class User:
|
|
249
|
+
"""
|
|
250
|
+
Класс пользователя
|
|
251
|
+
|
|
252
|
+
:param update: Обновление от MAX API
|
|
253
|
+
:type update: Dict[str, Any]
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
def __init__(self, update: Dict[str, Any]):
|
|
257
|
+
if update.get("callback"):
|
|
258
|
+
self.id = update.get("message").get("recipient").get("chat_id")
|
|
259
|
+
self.real_id = update.get("callback").get("user").get("user_id")
|
|
260
|
+
self.is_bot = update.get("callback").get("user").get("is_bot")
|
|
261
|
+
self.first_name = update.get("callback").get("user").get("first_name")
|
|
262
|
+
self.username = update.get("callback").get("user").get("name")
|
|
263
|
+
self.last_name = update.get("callback").get("user").get("last_name")
|
|
264
|
+
self.language_code = update.get("user_locale")
|
|
265
|
+
else:
|
|
266
|
+
self.id = update.get("message").get("recipient").get("chat_id")
|
|
267
|
+
self.real_id = update.get("message").get("sender").get("user_id")
|
|
268
|
+
self.is_bot = update.get("message").get("sender").get("is_bot")
|
|
269
|
+
self.first_name = update.get("message").get("sender").get("first_name")
|
|
270
|
+
self.username = update.get("message").get("sender").get("name")
|
|
271
|
+
self.last_name = update.get("message").get("sender").get("last_name")
|
|
272
|
+
self.language_code = update.get("user_locale")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class Chat:
|
|
276
|
+
"""
|
|
277
|
+
Класс чата
|
|
278
|
+
|
|
279
|
+
:param update: Обновление от MAX API
|
|
280
|
+
:type update: Dict[str, Any]
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
def __init__(self, update: Dict[str, Any]):
|
|
284
|
+
self.id = update.get("message").get("recipient").get("chat_id")
|
|
285
|
+
self.type = update.get("message").get("recipient").get("chat_type")
|
|
286
|
+
self.user_id = update.get("message").get("recipient").get("user_id")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class ChatLink:
|
|
290
|
+
"""
|
|
291
|
+
Класс ссылки на чат
|
|
292
|
+
|
|
293
|
+
:param update: Обновление от MAX API
|
|
294
|
+
:type update: Dict[str, Any]
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
def __init__(self, update: Dict[str, Any]):
|
|
298
|
+
self.id = update.get("chat_id")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class Link:
|
|
302
|
+
"""
|
|
303
|
+
Класс ссылки на сообщение
|
|
304
|
+
|
|
305
|
+
:param link: Словарь с данными ссылки
|
|
306
|
+
:type link: Dict[str, Any]
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
def __init__(self, link: Dict[str, Any]):
|
|
310
|
+
if link:
|
|
311
|
+
self.type = link.get("type")
|
|
312
|
+
self.message_id: str = None
|
|
313
|
+
self.from_user: Optional[User] = None
|
|
314
|
+
self.chat: ChatLink = ChatLink(update=link)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class Photo:
|
|
318
|
+
"""
|
|
319
|
+
Класс для работы с фотографиями
|
|
320
|
+
|
|
321
|
+
:param update: Обновление от MAX API
|
|
322
|
+
:type update: Dict[str, Any]
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
def __init__(self, update: Dict[str, Any]):
|
|
326
|
+
attach = update.get("message").get("body").get("attachments")
|
|
327
|
+
if attach:
|
|
328
|
+
for att in attach:
|
|
329
|
+
if att.get("type") == "image":
|
|
330
|
+
self.file_id = att.get("payload").get("photo_id")
|
|
331
|
+
self.token: str = att.get("payload").get("token")
|
|
332
|
+
self.url: str = att.get("payload").get("url")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class InputMedia:
|
|
336
|
+
"""
|
|
337
|
+
Класс формирования объекта attachments для отправки медиа
|
|
338
|
+
|
|
339
|
+
:param type: Тип медиа (photo/file)
|
|
340
|
+
:type type: str
|
|
341
|
+
|
|
342
|
+
:param media: Байты медиа
|
|
343
|
+
:type media: bytes
|
|
344
|
+
|
|
345
|
+
:param caption: Подпись к медиа
|
|
346
|
+
:type caption: Optional[str]
|
|
347
|
+
|
|
348
|
+
:param parse_mode: Режим парсинга текста (markdown/html)
|
|
349
|
+
:type parse_mode: Optional[str]
|
|
350
|
+
"""
|
|
351
|
+
compare_types = {
|
|
352
|
+
"photo": "image",
|
|
353
|
+
"file": "file"
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
def __init__(self, type: str = None, media: bytes = None, caption: str = None, parse_mode: str = None):
|
|
357
|
+
self.type = type if type else "photo"
|
|
358
|
+
self.media = media
|
|
359
|
+
self.caption = caption
|
|
360
|
+
self.parse_mode = parse_mode
|
|
361
|
+
self.api = None
|
|
362
|
+
|
|
363
|
+
def _get_upload_url(self, type_attach: str = "photo") -> Dict[str, Any]:
|
|
364
|
+
"""
|
|
365
|
+
Шаг 1. Получение URL для загрузки файла
|
|
366
|
+
|
|
367
|
+
:param type_attach: Тип вложения
|
|
368
|
+
:type type_attach: str
|
|
369
|
+
|
|
370
|
+
:return: Ответ API с URL для загрузки
|
|
371
|
+
:rtype: Dict[str, Any]
|
|
372
|
+
"""
|
|
373
|
+
return self.api.get_upload_file_url(type_attach=self.compare_types.get(type_attach))
|
|
374
|
+
|
|
375
|
+
def _load_file_to_max(self, url: str, file_name: str = None) -> Dict[str, Any]:
|
|
376
|
+
"""
|
|
377
|
+
Шаг 2. Загрузка файла на сервер MAX API
|
|
378
|
+
|
|
379
|
+
:param url: URL для загрузки
|
|
380
|
+
:type url: str
|
|
381
|
+
|
|
382
|
+
:param file_name: Имя файла
|
|
383
|
+
:type file_name: Optional[str]
|
|
384
|
+
|
|
385
|
+
:return: Ответ API после загрузки
|
|
386
|
+
:rtype: Dict[str, Any]
|
|
387
|
+
"""
|
|
388
|
+
if file_name:
|
|
389
|
+
files = {"data": (file_name, self.media, "text/plain")}
|
|
390
|
+
return self.api.load_file(url=url, files=files, content_types=None)
|
|
391
|
+
else:
|
|
392
|
+
files = {"data": self.media}
|
|
393
|
+
return self.api.load_file(url=url, files=files)
|
|
394
|
+
|
|
395
|
+
def to_dict(self, api: Api, file_name: str = None) -> Dict[str, Any]:
|
|
396
|
+
"""
|
|
397
|
+
Формирование attachments для отправки медиа
|
|
398
|
+
|
|
399
|
+
:param api: Объект API
|
|
400
|
+
:type api: Api
|
|
401
|
+
|
|
402
|
+
:param file_name: Имя файла
|
|
403
|
+
:type file_name: Optional[str]
|
|
404
|
+
|
|
405
|
+
:return: Словарь с данными вложения
|
|
406
|
+
:rtype: Dict[str, Any]
|
|
407
|
+
"""
|
|
408
|
+
self.api = api
|
|
409
|
+
upload_url = self._get_upload_url(type_attach=self.type).get("url")
|
|
410
|
+
if not upload_url:
|
|
411
|
+
return []
|
|
412
|
+
if is_pil_image(self.media):
|
|
413
|
+
self.media = pil_image_to_bytes(self.media)
|
|
414
|
+
load_file_result = self._load_file_to_max(url=upload_url, file_name=file_name)
|
|
415
|
+
if file_name:
|
|
416
|
+
token_dict = {"token": load_file_result.get("token")}
|
|
417
|
+
else:
|
|
418
|
+
token_dict = list(list(load_file_result.values())[0].values())[0]
|
|
419
|
+
return {
|
|
420
|
+
"type": self.compare_types.get(self.type),
|
|
421
|
+
"payload": token_dict
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class InputMediaPhoto(InputMedia):
|
|
426
|
+
"""
|
|
427
|
+
Класс для отправки фотографий
|
|
428
|
+
|
|
429
|
+
:param media: Байты изображения
|
|
430
|
+
:type media: bytes
|
|
431
|
+
|
|
432
|
+
:param caption: Подпись к фото
|
|
433
|
+
:type caption: Optional[str]
|
|
434
|
+
|
|
435
|
+
:param parse_mode: Режим парсинга текста
|
|
436
|
+
:type parse_mode: Optional[str]
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
def __init__(self, media=None, caption=None, parse_mode=None):
|
|
440
|
+
super().__init__(type="photo", media=media, caption=caption, parse_mode=parse_mode)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class InputMediaVideo(InputMedia):
|
|
444
|
+
"""
|
|
445
|
+
Класс для отправки видео
|
|
446
|
+
|
|
447
|
+
:param media: Байты видео
|
|
448
|
+
:type media: bytes
|
|
449
|
+
|
|
450
|
+
:param caption: Подпись к видео
|
|
451
|
+
:type caption: Optional[str]
|
|
452
|
+
|
|
453
|
+
:param parse_mode: Режим парсинга текста
|
|
454
|
+
:type parse_mode: Optional[str]
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
def __init__(self, media=None, caption=None, parse_mode=None):
|
|
458
|
+
super().__init__(type="video", media=media, caption=caption, parse_mode=parse_mode)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class Message:
|
|
462
|
+
"""
|
|
463
|
+
Класс для работы с сообщениями (аналог telebot.types.Message)
|
|
464
|
+
|
|
465
|
+
:param update: Обновление от MAX API
|
|
466
|
+
:type update: Dict[str, Any]
|
|
467
|
+
|
|
468
|
+
:param api: Объект API
|
|
469
|
+
:type api: Api
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
@staticmethod
|
|
473
|
+
def _get_photo_from_attachments(update: Dict[str, Any]) -> Optional[ImageAttachment]:
|
|
474
|
+
"""
|
|
475
|
+
Извлечение фото из вложений сообщения
|
|
476
|
+
|
|
477
|
+
:param update: Обновление от MAX API
|
|
478
|
+
:type update: Dict[str, Any]
|
|
479
|
+
|
|
480
|
+
:return: Объект ImageAttachment или None
|
|
481
|
+
:rtype: Optional[ImageAttachment]
|
|
482
|
+
"""
|
|
483
|
+
if update.get("message"):
|
|
484
|
+
update = update.get("message")
|
|
485
|
+
if update.get("body"):
|
|
486
|
+
update = update.get("body")
|
|
487
|
+
if update.get("attachments"):
|
|
488
|
+
attachs = update.get("attachments")
|
|
489
|
+
for attach in attachs:
|
|
490
|
+
if attach.get("type") == "image":
|
|
491
|
+
return ImageAttachment(attach=attach)
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
@staticmethod
|
|
495
|
+
def _get_content_type(update: Dict[str, Any]) -> str:
|
|
496
|
+
"""
|
|
497
|
+
Определение типа контента сообщения
|
|
498
|
+
|
|
499
|
+
:param update: Обновление от MAX API
|
|
500
|
+
:type update: Dict[str, Any]
|
|
501
|
+
|
|
502
|
+
:return: Тип контента
|
|
503
|
+
:rtype: str
|
|
504
|
+
"""
|
|
505
|
+
if update.get("message").get("body").get("attachments"):
|
|
506
|
+
c_type = update.get("message").get("body").get("attachments")[0].get("type")
|
|
507
|
+
if c_type == "image":
|
|
508
|
+
return "photo"
|
|
509
|
+
else:
|
|
510
|
+
return c_type
|
|
511
|
+
else:
|
|
512
|
+
return "text"
|
|
513
|
+
|
|
514
|
+
@staticmethod
|
|
515
|
+
def _get_msg_id(update: Dict[str, Any]) -> Optional[str]:
|
|
516
|
+
"""
|
|
517
|
+
Получение ID сообщения
|
|
518
|
+
|
|
519
|
+
:param update: Обновление от MAX API
|
|
520
|
+
:type update: Dict[str, Any]
|
|
521
|
+
|
|
522
|
+
:return: ID сообщения или None
|
|
523
|
+
:rtype: Optional[str]
|
|
524
|
+
"""
|
|
525
|
+
if update.get("message").get("body"):
|
|
526
|
+
return update.get("message").get("body").get("mid")
|
|
527
|
+
elif update.get("message").get("mid"):
|
|
528
|
+
return update.get("message").get("mid")
|
|
529
|
+
else:
|
|
530
|
+
return None
|
|
531
|
+
|
|
532
|
+
@staticmethod
|
|
533
|
+
def _get_msg_timestamp(update: Dict[str, Any]) -> Optional[datetime]:
|
|
534
|
+
"""
|
|
535
|
+
Получение времени сообщения
|
|
536
|
+
|
|
537
|
+
:param update: Обновление от MAX API
|
|
538
|
+
:type update: Dict[str, Any]
|
|
539
|
+
|
|
540
|
+
:return: Время сообщения или None
|
|
541
|
+
:rtype: Optional[datetime]
|
|
542
|
+
"""
|
|
543
|
+
if update.get("timestamp"):
|
|
544
|
+
time = str(update.get("timestamp"))
|
|
545
|
+
main_time = time[:10]
|
|
546
|
+
milisec = time[10:]
|
|
547
|
+
alltime = float(main_time + "." + milisec)
|
|
548
|
+
return datetime.fromtimestamp(alltime)
|
|
549
|
+
else:
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
@staticmethod
|
|
553
|
+
def _get_msg_text(update: Dict[str, Any]) -> Optional[str]:
|
|
554
|
+
"""
|
|
555
|
+
Получение текста сообщения
|
|
556
|
+
|
|
557
|
+
:param update: Обновление от MAX API
|
|
558
|
+
:type update: Dict[str, Any]
|
|
559
|
+
|
|
560
|
+
:return: Текст сообщения или None
|
|
561
|
+
:rtype: Optional[str]
|
|
562
|
+
"""
|
|
563
|
+
if update.get("message").get("body"):
|
|
564
|
+
return update.get("message").get("body").get("text")
|
|
565
|
+
else:
|
|
566
|
+
return None
|
|
567
|
+
|
|
568
|
+
def __init__(self, update: Dict[str, Any], api: Api):
|
|
569
|
+
"""
|
|
570
|
+
Инициализация объекта сообщения
|
|
571
|
+
"""
|
|
572
|
+
self.update = update
|
|
573
|
+
self.api = api
|
|
574
|
+
self.content_type: str = self._get_content_type(update=update)
|
|
575
|
+
self.id: Optional[str] = self._get_msg_id(update=update)
|
|
576
|
+
self.message_id: Optional[str] = self._get_msg_id(update=update)
|
|
577
|
+
self.from_user: Optional[User] = User(update=update)
|
|
578
|
+
self.date: Optional[datetime] = self._get_msg_timestamp(update=update)
|
|
579
|
+
self.chat: Chat = Chat(update=update)
|
|
580
|
+
self.reply_to_message: Link = Link(link=update.get("message").get("link"))
|
|
581
|
+
self.text: Optional[str] = self._get_msg_text(update=update)
|
|
582
|
+
self.photo: Optional[ImageAttachment] = self._get_photo_from_attachments(update=update)
|
|
583
|
+
self.photo_reply: Photo = Photo(update=update)
|
|
584
|
+
self.update_type = update.get('update_type')
|
|
585
|
+
|
|
586
|
+
# def reply(self, text: str, **kwargs) -> Dict[str, Any]:
|
|
587
|
+
# """
|
|
588
|
+
# Ответ на текущее сообщение
|
|
589
|
+
#
|
|
590
|
+
# :param text: Текст ответа
|
|
591
|
+
# :type text: str
|
|
592
|
+
#
|
|
593
|
+
# :param kwargs: Дополнительные параметры:
|
|
594
|
+
# - parse_mode: Режим парсинга (markdown/html)
|
|
595
|
+
# - reply_markup: Клавиатура
|
|
596
|
+
# - attachments: Вложения
|
|
597
|
+
#
|
|
598
|
+
# :return: Ответ API
|
|
599
|
+
# :rtype: Dict[str, Any]
|
|
600
|
+
# """
|
|
601
|
+
# attachments = kwargs.get('attachments', [])
|
|
602
|
+
# reply_markup = kwargs.get('reply_markup')
|
|
603
|
+
#
|
|
604
|
+
# if reply_markup:
|
|
605
|
+
# attachments.append(reply_markup.to_attachment())
|
|
606
|
+
#
|
|
607
|
+
# return self.api.send_message(
|
|
608
|
+
# chat_id=self.chat.id,
|
|
609
|
+
# text=text,
|
|
610
|
+
# attachments=attachments,
|
|
611
|
+
# link=self.message_id
|
|
612
|
+
# )
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class CallbackQuery:
|
|
616
|
+
"""
|
|
617
|
+
Класс для обработки callback-запросов от inline-кнопок
|
|
618
|
+
|
|
619
|
+
:param update: Обновление от MAX API с типом 'message_callback'
|
|
620
|
+
:type update: Dict[str, Any]
|
|
621
|
+
|
|
622
|
+
:param api: Объект API для отправки ответов
|
|
623
|
+
:type api: Api
|
|
624
|
+
"""
|
|
625
|
+
|
|
626
|
+
def __init__(self, update: Dict[str, Any], api: Api):
|
|
627
|
+
self.api = api
|
|
628
|
+
cb = update.get("callback", {})
|
|
629
|
+
self.id: str = cb.get("callback_id", "")
|
|
630
|
+
self.chat_instance: str = self.id
|
|
631
|
+
self.data: Optional[str] = cb.get("payload")
|
|
632
|
+
|
|
633
|
+
# Если нет в callback, пытаемся извлечь из message attachments
|
|
634
|
+
if not self.data:
|
|
635
|
+
self.data = self._extract_button_data_from_message(update)
|
|
636
|
+
|
|
637
|
+
msg = update.get("message", {})
|
|
638
|
+
self.from_user = User(update=update) if msg else None
|
|
639
|
+
self.message = Message(update=update, api=api) if msg else None
|
|
640
|
+
|
|
641
|
+
def _extract_button_data_from_message(self, update: Dict[str, Any]) -> Optional[str]:
|
|
642
|
+
"""
|
|
643
|
+
Извлечение данных кнопки из message attachments
|
|
644
|
+
|
|
645
|
+
:param update: Обновление от MAX API
|
|
646
|
+
:type update: Dict[str, Any]
|
|
647
|
+
|
|
648
|
+
:return: Данные кнопки или None если не найдены
|
|
649
|
+
:rtype: Optional[str]
|
|
650
|
+
"""
|
|
651
|
+
callback_id = self.id
|
|
652
|
+
message = update.get("message", {})
|
|
653
|
+
|
|
654
|
+
if not message:
|
|
655
|
+
return None
|
|
656
|
+
|
|
657
|
+
body = message.get("body", {})
|
|
658
|
+
attachments = body.get("attachments", [])
|
|
659
|
+
|
|
660
|
+
for attachment in attachments:
|
|
661
|
+
if attachment.get("callback_id") == callback_id:
|
|
662
|
+
payload = attachment.get("payload", {})
|
|
663
|
+
buttons = payload.get("buttons", [])
|
|
664
|
+
|
|
665
|
+
for row in buttons:
|
|
666
|
+
for button in row:
|
|
667
|
+
if button.get("type") == "callback":
|
|
668
|
+
return button.get("text", "unknown")
|
|
669
|
+
|
|
670
|
+
return None
|
|
671
|
+
|
|
672
|
+
def answer(self, text: Optional[str] = None, **kwargs) -> Dict[str, Any]:
|
|
673
|
+
"""
|
|
674
|
+
Ответ на нажатие inline-кнопки в Max API
|
|
675
|
+
|
|
676
|
+
:param text: Текст для обновления сообщения
|
|
677
|
+
:type text: Optional[str]
|
|
678
|
+
|
|
679
|
+
:param kwargs: Дополнительные параметры:
|
|
680
|
+
- notification: Текст уведомления для пользователя
|
|
681
|
+
- attachments: Вложения для обновления сообщения
|
|
682
|
+
- link: Ссылка на сообщение для reply/forward
|
|
683
|
+
- notify: Отправлять ли уведомление о редактировании
|
|
684
|
+
- format: Формат текста (markdown/html)
|
|
685
|
+
|
|
686
|
+
:return: Ответ от API
|
|
687
|
+
:rtype: Dict[str, Any]
|
|
688
|
+
"""
|
|
689
|
+
notification = kwargs.pop('notification', None)
|
|
690
|
+
attachments = kwargs.pop('attachments', None)
|
|
691
|
+
link = kwargs.pop('link', None)
|
|
692
|
+
notify = kwargs.pop('notify', True)
|
|
693
|
+
format = kwargs.pop('format', None)
|
|
694
|
+
|
|
695
|
+
should_update_message = (text is not None or
|
|
696
|
+
attachments is not None or
|
|
697
|
+
link is not None)
|
|
698
|
+
|
|
699
|
+
try:
|
|
700
|
+
if not should_update_message and notification:
|
|
701
|
+
return self.api.answer_callback(
|
|
702
|
+
callback_id=self.id,
|
|
703
|
+
notification=notification
|
|
704
|
+
)
|
|
705
|
+
else:
|
|
706
|
+
return self.api.answer_callback(
|
|
707
|
+
callback_id=self.id,
|
|
708
|
+
text=text,
|
|
709
|
+
notification=notification,
|
|
710
|
+
attachments=attachments,
|
|
711
|
+
link=link,
|
|
712
|
+
notify=notify,
|
|
713
|
+
format=format
|
|
714
|
+
)
|
|
715
|
+
except Exception as e:
|
|
716
|
+
if notification:
|
|
717
|
+
try:
|
|
718
|
+
return self.api.answer_callback(
|
|
719
|
+
callback_id=self.id,
|
|
720
|
+
notification=notification
|
|
721
|
+
)
|
|
722
|
+
except:
|
|
723
|
+
return {"success": False, "error": str(e)}
|
|
724
|
+
return {"success": False, "error": str(e)}
|
|
725
|
+
|
|
726
|
+
def answer_notification(self, text: str) -> Dict[str, Any]:
|
|
727
|
+
"""
|
|
728
|
+
Отправка только уведомления (без изменения сообщения)
|
|
729
|
+
|
|
730
|
+
:param text: Текст уведомления
|
|
731
|
+
:type text: str
|
|
732
|
+
|
|
733
|
+
:return: Ответ от API
|
|
734
|
+
:rtype: Dict[str, Any]
|
|
735
|
+
"""
|
|
736
|
+
return self.answer(notification=text)
|
|
737
|
+
|
|
738
|
+
def answer_update(self, text: str, **kwargs) -> Dict[str, Any]:
|
|
739
|
+
"""
|
|
740
|
+
Обновление сообщения с опциональным уведомлением
|
|
741
|
+
|
|
742
|
+
:param text: Текст для обновления сообщения
|
|
743
|
+
:type text: str
|
|
744
|
+
|
|
745
|
+
:param kwargs: Дополнительные параметры
|
|
746
|
+
:type kwargs: Dict[str, Any]
|
|
747
|
+
|
|
748
|
+
:return: Ответ от API
|
|
749
|
+
:rtype: Dict[str, Any]
|
|
750
|
+
"""
|
|
751
|
+
if 'notification' not in kwargs:
|
|
752
|
+
kwargs['notification'] = "Обновлено!"
|
|
753
|
+
return self.answer(text=text, **kwargs)
|