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 ADDED
@@ -0,0 +1,644 @@
1
+ import asyncio
2
+ import json
3
+ import re
4
+ import traceback
5
+
6
+ from typing import Dict, Any, List, Optional, Callable, Union
7
+
8
+ from maxibot.apihelper import Api
9
+ from maxibot.types import Message, CallbackQuery, InputMedia
10
+ from maxibot.types import UpdateType, InlineKeyboardMarkup
11
+ from maxibot.util import extract_command, get_text, get_parce_mode
12
+ from maxibot.core.attachments.photo import Photo
13
+ from maxibot.core.network.polling import Polling
14
+
15
+ HandlerFunc = Callable[[Message], None]
16
+
17
+
18
+ class MaxiBot:
19
+ """
20
+ Главный класс бота
21
+ """
22
+ def __init__(self, token: str):
23
+ """
24
+ Метод инициализации бота
25
+ :param token: Токен бота
26
+ :type token: str
27
+ """
28
+ self.api = Api(token=token)
29
+ self.handlers = {
30
+ "update": [], # Общие обработчики для всех типов обновлений
31
+ UpdateType.MESSAGE_CREATED: [],
32
+ UpdateType.MESSAGE_CALLBACK: [],
33
+ UpdateType.BOT_STARTED: [],
34
+ UpdateType.MESSAGE_EDITED: [],
35
+ UpdateType.MESSAGE_DELETED: [],
36
+ UpdateType.MESSAGE_CHAT_CREATED: [],
37
+ }
38
+ self.message_handlers = []
39
+ self.callback_query_handlers = []
40
+ self.poll = None
41
+ self.is_running = False
42
+ self.count_retries = 10
43
+
44
+ @staticmethod
45
+ def _build_handler_dict(handler: HandlerFunc, **filters):
46
+ """
47
+ Функция, которая формирует словарь для добавления в список обработчиков событий (handler)
48
+
49
+ :param handler: Description
50
+ :type handler: HandlerFunc
51
+ :param filters: Description
52
+ """
53
+ return {
54
+ 'function': handler,
55
+ 'filters': {ftype: fvalue for ftype, fvalue in filters.items() if fvalue is not None}
56
+ }
57
+
58
+ def polling(self, allowed_updates: Optional[List[str]] = None):
59
+ """
60
+ Функция, которая запускает корутину
61
+ """
62
+ asyncio.run(self.start(allowed_updates=allowed_updates))
63
+
64
+ def stop(self):
65
+ """
66
+ Метод останавливает поллинг бота
67
+ """
68
+ if not self.is_running:
69
+ print("Bot is not running")
70
+ return None
71
+ if self.poll:
72
+ self.poll.stop()
73
+ self.is_running = False
74
+
75
+ async def start(self, allowed_updates: Optional[List[str]] = None):
76
+ """
77
+ Метод запускает получение обновлений по боту
78
+
79
+ :param allowed_updates: Description
80
+ :type allowed_updates: Optional[List[str]]
81
+ """
82
+ if self.is_running:
83
+ print("Bot is already running")
84
+ return None
85
+ self.is_running = True
86
+ self.poll = Polling(api=self.api, allowed_updates=allowed_updates)
87
+ await self.poll.loop(self._process_update)
88
+
89
+ # def on(self, update_type: str):
90
+ # """
91
+ # Декоратор для регистрации обработчика определенного типа обновлений
92
+
93
+ # :param update_type: Тип обновления (см. UpdateType)
94
+ # """
95
+ # def decorator(func: HandlerFunc):
96
+ # self.handlers.setdefault(update_type, []).append(func)
97
+ # return func
98
+ # return decorator
99
+
100
+ def message_handler(
101
+ self,
102
+ commands: Optional[List[str]] = None,
103
+ regexp: Optional[str] = None,
104
+ func: Optional[Callable] = None,
105
+ content_types: Optional[List[str]] = None,
106
+ ):
107
+ """
108
+ Декоратор для регистрации обработчика текстовых сообщений по шаблону
109
+
110
+ :param pattern: Шаблон текста (точное совпадение или регулярное выражение)
111
+ :type pattern: str
112
+ """
113
+ def decorator(funcs: HandlerFunc):
114
+ handler_dict = self._build_handler_dict(
115
+ funcs,
116
+ commands=commands,
117
+ regexp=regexp,
118
+ func=func,
119
+ content_types=content_types
120
+ )
121
+ self.message_handlers.append(handler_dict)
122
+ return funcs
123
+ return decorator
124
+
125
+ def run_handler(self, context: Message, message_handlers: List[Dict]):
126
+ """
127
+ Метод запуска обработчиков событий текстового сообщения
128
+
129
+ :param context: Description
130
+ :type context: Context
131
+ """
132
+ for handler in message_handlers:
133
+ if self._check_filters(context=context, handler=handler):
134
+ handler.get("function")(context)
135
+ break
136
+
137
+ def _test_filter(self, message_filter: str, filter_value: List, context: Message):
138
+ """
139
+ Метод проверки соответствия сообщения всем фильтрам текстовых сообщений
140
+
141
+ :param message_filter: Description
142
+ :type message_filter: str
143
+ :param filter_value: Description
144
+ :type filter_value: List
145
+ :param context: Description
146
+ :type context: Context
147
+ """
148
+
149
+ text = context.text
150
+ if message_filter == 'content_types':
151
+ return context.content_type in filter_value
152
+ if message_filter == 'regexp':
153
+ return re.search(filter_value, text, re.IGNORECASE)
154
+ elif message_filter == 'commands':
155
+ return extract_command(text) in filter_value
156
+ # elif message_filter == 'chat_types':
157
+ # return context.chat.type in filter_value
158
+ elif message_filter == 'func':
159
+ # print("FUUUUUUUUUUUUUUUUUUUUUUUUNCCCCCCCCCCCCCCCCC")
160
+ return filter_value(context)
161
+ return False
162
+
163
+ def _check_filters(self, context, handler: Dict):
164
+ """
165
+ Проверка текстового сообщения на фильтры
166
+
167
+ :param context: Сообщение
168
+ :type context: Context
169
+ """
170
+ if handler['filters']:
171
+ if isinstance(context, CallbackQuery):
172
+ # Сначала проверяем фильтр по data
173
+ if 'data' in handler['filters']:
174
+ filter_data = handler['filters']['data']
175
+ if context.data != filter_data:
176
+ return False
177
+ func_filter = handler['filters'].get('func')
178
+ if func_filter:
179
+ try:
180
+ return func_filter(context)
181
+ except Exception as e:
182
+ print(f"Error in filter function: {e}")
183
+ return False
184
+
185
+ return True
186
+ elif isinstance(context, Message):
187
+ for message_filter, filter_value in handler['filters'].items():
188
+ if filter_value is None:
189
+ continue
190
+ if not self._test_filter(message_filter, filter_value, context):
191
+ return False
192
+ return True
193
+ return False
194
+
195
+ def _process_text_message(self, context: Message):
196
+ """
197
+ Обрабатывает входящее сообщение
198
+
199
+ :param context: Контекст обновления
200
+ :type context: Context
201
+ """
202
+ # if text.startswith("/"):
203
+ # print("Command send. Do nothing now))")
204
+ self.run_handler(context=context, message_handlers=self.message_handlers)
205
+ # for pattern, handler in self.message_handlers:
206
+ # if pattern == text or re.search(pattern, text):
207
+ # handler(context)
208
+
209
+ def _process_update(self, update: Dict[str, Any]):
210
+ """
211
+ Метод для обработки входящего полученного обновления
212
+
213
+ :param update: Данные по обновлениям
214
+ :type update: Dict[str, Any]
215
+ """
216
+ try:
217
+ # print("===============\nUPDATE RECEIVED\n===============")
218
+ # print(f"Update type: {update.get('update_type')}")
219
+ # print(f"Full update: {json.dumps(update, indent=2)}")
220
+
221
+ update_type = update.get("update_type")
222
+ if update_type == UpdateType.MESSAGE_CREATED and "message" in update.keys():
223
+ context = Message(update, self.api)
224
+ self._process_text_message(context)
225
+ elif update_type == UpdateType.MESSAGE_CALLBACK:
226
+ print("Processing message_callback...")
227
+ if "callback" in update:
228
+ callback = CallbackQuery(update, self.api)
229
+ # print(f"Created callback: id={callback.id}, data={callback.data}")
230
+ self._process_callback_query(callback)
231
+ except Exception:
232
+ print(f"Error while processing update: {traceback.format_exc()}")
233
+
234
+ def _check_text_length(self, text):
235
+ """
236
+ Проверки длины строки
237
+ """
238
+ return text is not None and not (len(text) < 4000)
239
+
240
+ def send_photo(
241
+ self,
242
+ chat_id: Union[int, str],
243
+ photo: Union[Any, str],
244
+ caption: Optional[str] = None,
245
+ parse_mode: Optional[str] = None,
246
+ reply_markup: Union[InlineKeyboardMarkup, Any] = None
247
+ ):
248
+ """
249
+ Отправляет сообщение с фото
250
+
251
+ :param chat_id: Чат, куда надо отправить сообщение
252
+ :type chat_id: Union[int, str]
253
+
254
+ :param photo: Объект фото
255
+ :type photo: Union[Any, str]
256
+
257
+ :param caption: Текст сообщения под фото
258
+ :type caption: Optional[str]
259
+
260
+ :param parse_mode: Разметка сообщения
261
+ :type parse_mode: Optional[str]
262
+
263
+ :return: Информация об отправленном сообщении
264
+ :rtype: Dict[str, Any]
265
+ """
266
+
267
+ if self._check_text_length(text=caption):
268
+ raise ValueError(f'caption должен быть меньше 4000 символов.\nСейчас их {len(caption)}')
269
+ final_attachments = []
270
+ if isinstance(photo, InputMedia):
271
+ final_attachments.append(photo.to_dict(api=self.api))
272
+ final_attachments.append(InputMedia(media=photo).to_dict(api=self.api))
273
+ if reply_markup:
274
+ if hasattr(reply_markup, 'to_attachment'):
275
+ final_attachments.append(reply_markup.to_attachment())
276
+ else:
277
+ final_attachments.append(reply_markup)
278
+ return Message(
279
+ update=self.api.send_message(
280
+ chat_id=chat_id,
281
+ text=caption,
282
+ attachments=final_attachments,
283
+ parse_mode=parse_mode
284
+ ),
285
+ api=self.api
286
+ )
287
+
288
+ def send_media_group(
289
+ self,
290
+ chat_id: Union[int, str],
291
+ media: list,
292
+ caption: Optional[str] = None,
293
+ parse_mode: Optional[str] = None,
294
+ reply_markup: Union[InlineKeyboardMarkup, Any] = None
295
+ ):
296
+ """
297
+ Отправляет сообщение с фото
298
+
299
+ :param chat_id: Чат, куда надо отправить сообщение
300
+ :type chat_id: Union[int, str]
301
+
302
+ :param photo: Объект фото
303
+ :type photo: Union[Any, str]
304
+
305
+ :param caption: Текст сообщения под фото
306
+ :type caption: Optional[str]
307
+
308
+ :param parse_mode: Разметка сообщения
309
+ :type parse_mode: Optional[str]
310
+
311
+ :return: Информация об отправленном сообщении
312
+ :rtype: Dict[str, Any]
313
+ """
314
+
315
+ if self._check_text_length(text=caption):
316
+ raise ValueError(f'caption должен быть меньше 4000 символов.\nСейчас их {len(caption)}')
317
+ final_attachments = []
318
+ for photo in media:
319
+ if isinstance(photo, InputMedia):
320
+ final_attachments.append(photo.to_dict(api=self.api))
321
+ else:
322
+ final_attachments.append(InputMedia(media=photo).to_dict(api=self.api))
323
+ if reply_markup:
324
+ if hasattr(reply_markup, 'to_attachment'):
325
+ final_attachments.append(reply_markup.to_attachment())
326
+ else:
327
+ final_attachments.append(reply_markup)
328
+ return Message(
329
+ update=self.api.send_message(
330
+ chat_id=chat_id,
331
+ text=caption,
332
+ attachments=final_attachments,
333
+ parse_mode=parse_mode
334
+ ),
335
+ api=self.api
336
+ )
337
+
338
+ def send_document(
339
+ self,
340
+ chat_id: Union[int, str],
341
+ document: Union[Any, str],
342
+ caption: Optional[str] = None,
343
+ parse_mode: Optional[str] = None,
344
+ reply_markup: Union[InlineKeyboardMarkup, Any] = None,
345
+ visible_file_name: Optional[str] = None
346
+ ):
347
+ """
348
+ Отправляет сообщение с файлом
349
+
350
+ :param chat_id: Чат, куда надо отправить сообщение
351
+ :type chat_id: Union[int, str]
352
+
353
+ :param document: Объект файла
354
+ :type document: Union[Any, str]
355
+
356
+ :param caption: Текст сообщения под фото
357
+ :type caption: Optional[str]
358
+
359
+ :param parse_mode: Разметка сообщения
360
+ :type parse_mode: Optional[str]
361
+
362
+ :return: Информация об отправленном сообщении
363
+ :rtype: Dict[str, Any]
364
+ """
365
+
366
+ if self._check_text_length(text=caption):
367
+ raise ValueError(f'caption должен быть меньше 4000 символов.\nСейчас их {len(caption)}')
368
+ final_attachments = []
369
+ if isinstance(document, InputMedia) and document.type == "file":
370
+ final_attachments.append(document.to_dict(api=self.api))
371
+ else:
372
+ final_attachments.append(
373
+ InputMedia(type="file", media=document).to_dict(api=self.api, file_name=visible_file_name)
374
+ )
375
+ if reply_markup:
376
+ if hasattr(reply_markup, 'to_attachment'):
377
+ final_attachments.append(reply_markup.to_attachment())
378
+ else:
379
+ final_attachments.append(reply_markup)
380
+ for _ in range(self.count_retries):
381
+ response = self.api.send_message(
382
+ chat_id=chat_id,
383
+ text=caption,
384
+ attachments=final_attachments,
385
+ parse_mode=parse_mode.lower()
386
+ )
387
+ if isinstance(response, str):
388
+ continue
389
+ break
390
+ return Message(update=response, api=self.api)
391
+
392
+ def delete_message(
393
+ self,
394
+ chat_id: Union[str, int],
395
+ message_id: str,
396
+ ):
397
+ """
398
+ Метод удаления сообщения `message_id` в чате `chat_id`
399
+
400
+ :param chat_id: Айди чата
401
+ :type chat_id: Union[str, int]
402
+
403
+ :param message_id: Айди сообщения
404
+ :type message_id: int
405
+ """
406
+ self.api.send_message(msg_id=message_id, method="DELETE")
407
+ return {}
408
+
409
+ def edit_message_text(
410
+ self,
411
+ text: str,
412
+ chat_id: Union[str, int],
413
+ message_id: str,
414
+ reply_markup: Union[InlineKeyboardMarkup, Any] = None,
415
+ parce_mode: Union[str, Any] = None
416
+ ):
417
+ """
418
+ Метод изменения текстового сообщения `message_id` в чате `chat_id`
419
+
420
+ :param text: Текст, на который надо заменить текущий
421
+ :type text: str
422
+
423
+ :param chat_id: Айди чата
424
+ :type chat_id: Union[str, int]
425
+
426
+ :param message_id: Айди сообщения
427
+ :type message_id: int
428
+ """
429
+ final_attachments = []
430
+ if reply_markup:
431
+ if hasattr(reply_markup, 'to_attachment'):
432
+ final_attachments.append(reply_markup.to_attachment())
433
+ else:
434
+ final_attachments.append(reply_markup)
435
+ self.api.send_message(
436
+ msg_id=message_id,
437
+ text=text,
438
+ method="PUT",
439
+ attachments=final_attachments,
440
+ parse_mode=parce_mode
441
+ )
442
+ return {}
443
+
444
+ def edit_message_media(
445
+ self,
446
+ media: Any,
447
+ chat_id: Union[str, int],
448
+ message_id: str,
449
+ reply_markup: Union[InlineKeyboardMarkup, Any] = None,
450
+ parce_mode: Union[str, Any] = "markdown"
451
+ ):
452
+ """
453
+ Метод изменения медиа сообщения `message_id` в чате `chat_id`
454
+
455
+ :param media: Медиа, на которое надо заменить текущее
456
+ :type media: str
457
+
458
+ :param chat_id: Айди чата
459
+ :type chat_id: Union[str, int]
460
+
461
+ :param message_id: Айди сообщения
462
+ :type message_id: int
463
+ """
464
+ final_attachments = []
465
+ # if isinstance(media, Photo):
466
+ # final_attachments.append(media.to_dict())
467
+ if isinstance(media, InputMedia):
468
+ final_attachments.append(media.to_dict(api=self.api))
469
+ else:
470
+ final_attachments.append(InputMedia(media=media).to_dict(api=self.api))
471
+ if reply_markup:
472
+ if hasattr(reply_markup, 'to_attachment'):
473
+ final_attachments.append(reply_markup.to_attachment())
474
+ else:
475
+ final_attachments.append(reply_markup)
476
+ text = get_text(media=media)
477
+ parce_mode = get_parce_mode(media=media, parse_mode=parce_mode)
478
+ self.api.send_message(
479
+ msg_id=message_id,
480
+ text=text,
481
+ method="PUT",
482
+ attachments=final_attachments,
483
+ parse_mode=parce_mode
484
+ )
485
+ return {}
486
+
487
+ def edit_message_reply_markup(
488
+ self,
489
+ chat_id: Union[str, int],
490
+ message_id: str,
491
+ reply_markup: Union[InlineKeyboardMarkup, Any] = None,
492
+ parce_mode: Union[str, Any] = "markdown"
493
+ ):
494
+ """
495
+ Метод изменения клавиатуры сообщения `message_id` в чате `chat_id`
496
+
497
+ :param chat_id: Айди чата
498
+ :type chat_id: Union[str, int]
499
+
500
+ :param message_id: Айди сообщения
501
+ :type message_id: int
502
+
503
+ :param reply_markup: Новая клавиатура
504
+ :type reply_markup: Union[InlineKeyboardMarkup, Any]
505
+ """
506
+ final_attachments = []
507
+ msg: Message = self.get_message(message_id=message_id)
508
+ if msg.photo:
509
+ final_attachments.append(msg.photo.to_dict())
510
+ if reply_markup:
511
+ if hasattr(reply_markup, 'to_attachment'):
512
+ final_attachments.append(reply_markup.to_attachment())
513
+ else:
514
+ final_attachments.append(reply_markup)
515
+ self.api.send_message(
516
+ msg_id=message_id,
517
+ method="PUT",
518
+ attachments=final_attachments,
519
+ parse_mode=parce_mode.lower()
520
+ )
521
+ return {}
522
+
523
+ def send_message(
524
+ self,
525
+ chat_id: Union[str, int],
526
+ text: str,
527
+ attachments: Optional[List[Dict[str, Any]]] = None,
528
+ reply_markup: Optional[Any] = None,
529
+ parce_mode: str = "markdown",
530
+ notify: bool = True
531
+ ) -> Message:
532
+ """
533
+ Отправляет ответ на текущее сообщение/обновление
534
+
535
+ :param text: Текст сообщения
536
+ :type text:
537
+
538
+ :param attachments: Вложения сообщения
539
+ :type attachments:
540
+
541
+ :param keyboard: Объект клавиатуры (будет добавлен к attachments)
542
+ :type keyboard:
543
+
544
+ :return: Информация об отправленном сообщении
545
+ :rtype: Message
546
+ """
547
+ if self._check_text_length(text=text):
548
+ raise ValueError(f'text должен быть меньше 4000 символов\nСейчас их {len(text)}')
549
+ if isinstance(chat_id, int):
550
+ chat_id = str(chat_id)
551
+
552
+ final_attachments = attachments.copy() if attachments else []
553
+
554
+ # Если передана клавиатура, добавляем её как вложение
555
+ if reply_markup:
556
+ if hasattr(reply_markup, 'to_attachment'):
557
+ final_attachments.append(reply_markup.to_attachment())
558
+ else:
559
+ final_attachments.append(reply_markup)
560
+
561
+ return Message(
562
+ update=self.api.send_message(
563
+ chat_id=chat_id,
564
+ text=text,
565
+ attachments=final_attachments,
566
+ parse_mode=parce_mode.lower(),
567
+ notify=notify
568
+ ),
569
+ api=self.api
570
+ )
571
+
572
+ def get_message(self, message_id: str):
573
+ """
574
+ Метод получения сообщения по айди
575
+
576
+ :param message_id: Айди сообщения
577
+ :type message_id: str
578
+ """
579
+ msg = self.api.get_message(msg_id=message_id)
580
+ update = {"update_type": "get_message"}
581
+ update["message"] = msg
582
+ return Message(update=update, api=self.api)
583
+
584
+ def callback_query_handler(self, data=None, **kwargs):
585
+ """
586
+ Декоратор для регистрации обработчиков callback-запросов от inline-кнопок
587
+
588
+ :param data: Данные кнопки для фильтрации (callback_data)
589
+ :type data: Optional[str]
590
+
591
+ :param kwargs: Дополнительные фильтры для обработчика
592
+
593
+ :return: Декоратор для функции-обработчика
594
+ :rtype: Callable
595
+
596
+ Пример использования:
597
+ @bot.callback_query_handler(func=lambda cb: cb.data == "yes")
598
+ def yes_handler(callback):
599
+ callback.answer(notification="да да")
600
+ """
601
+ def decorator(handler):
602
+ filters = {}
603
+ if data:
604
+ filters['data'] = data
605
+ filters.update(kwargs)
606
+
607
+ handler_dict = self._build_handler_dict(handler, **filters)
608
+ self.callback_query_handlers.append(handler_dict)
609
+ return handler
610
+
611
+ return decorator
612
+
613
+ def add_callback_query_handler(self, handler_dict):
614
+ """
615
+ Добавляет обработчик callback-запросов напрямую
616
+
617
+ :param handler_dict: Словарь с описанием обработчика
618
+ :type handler_dict: Dict[str, Any]
619
+
620
+ :return: None
621
+ """
622
+ self.callback_query_handlers.append(handler_dict)
623
+
624
+ def _process_callback_query(self, callback: CallbackQuery):
625
+ """
626
+ Обрабатывает входящий callback-запрос
627
+ Метод ищет подходящий обработчик среди зарегистрированных и вызывает первый соответствующий фильтрам
628
+
629
+ :param callback: Объект callback-запроса
630
+ :type callback: CallbackQuery
631
+
632
+ :return: None
633
+ """
634
+ # print(f"Processing callback: id={callback.id}, data={callback.data}")
635
+ # print(f"Callback user: {callback.from_user}")
636
+
637
+ for handler in self.callback_query_handlers:
638
+ # print(f"Checking handler with filters: {handler['filters']}")
639
+ if self._check_filters(callback, handler):
640
+ # print("Handler matched! Calling function...")
641
+ handler["function"](callback)
642
+ break
643
+ else:
644
+ print("No matching handler found for callback")