chibi-bot 1.6.0b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. chibi/__init__.py +0 -0
  2. chibi/__main__.py +343 -0
  3. chibi/cli.py +90 -0
  4. chibi/config/__init__.py +6 -0
  5. chibi/config/app.py +123 -0
  6. chibi/config/gpt.py +108 -0
  7. chibi/config/logging.py +15 -0
  8. chibi/config/telegram.py +43 -0
  9. chibi/config_generator.py +233 -0
  10. chibi/constants.py +362 -0
  11. chibi/exceptions.py +58 -0
  12. chibi/models.py +496 -0
  13. chibi/schemas/__init__.py +0 -0
  14. chibi/schemas/anthropic.py +20 -0
  15. chibi/schemas/app.py +54 -0
  16. chibi/schemas/cloudflare.py +65 -0
  17. chibi/schemas/mistralai.py +56 -0
  18. chibi/schemas/suno.py +83 -0
  19. chibi/service.py +135 -0
  20. chibi/services/bot.py +276 -0
  21. chibi/services/lock_manager.py +20 -0
  22. chibi/services/mcp/manager.py +242 -0
  23. chibi/services/metrics.py +54 -0
  24. chibi/services/providers/__init__.py +16 -0
  25. chibi/services/providers/alibaba.py +79 -0
  26. chibi/services/providers/anthropic.py +40 -0
  27. chibi/services/providers/cloudflare.py +98 -0
  28. chibi/services/providers/constants/suno.py +2 -0
  29. chibi/services/providers/customopenai.py +11 -0
  30. chibi/services/providers/deepseek.py +15 -0
  31. chibi/services/providers/eleven_labs.py +85 -0
  32. chibi/services/providers/gemini_native.py +489 -0
  33. chibi/services/providers/grok.py +40 -0
  34. chibi/services/providers/minimax.py +96 -0
  35. chibi/services/providers/mistralai_native.py +312 -0
  36. chibi/services/providers/moonshotai.py +20 -0
  37. chibi/services/providers/openai.py +74 -0
  38. chibi/services/providers/provider.py +892 -0
  39. chibi/services/providers/suno.py +130 -0
  40. chibi/services/providers/tools/__init__.py +23 -0
  41. chibi/services/providers/tools/cmd.py +132 -0
  42. chibi/services/providers/tools/common.py +127 -0
  43. chibi/services/providers/tools/constants.py +78 -0
  44. chibi/services/providers/tools/exceptions.py +1 -0
  45. chibi/services/providers/tools/file_editor.py +875 -0
  46. chibi/services/providers/tools/mcp_management.py +274 -0
  47. chibi/services/providers/tools/mcp_simple.py +72 -0
  48. chibi/services/providers/tools/media.py +451 -0
  49. chibi/services/providers/tools/memory.py +252 -0
  50. chibi/services/providers/tools/schemas.py +10 -0
  51. chibi/services/providers/tools/send.py +435 -0
  52. chibi/services/providers/tools/tool.py +163 -0
  53. chibi/services/providers/tools/utils.py +146 -0
  54. chibi/services/providers/tools/web.py +261 -0
  55. chibi/services/providers/utils.py +182 -0
  56. chibi/services/task_manager.py +93 -0
  57. chibi/services/user.py +269 -0
  58. chibi/storage/abstract.py +54 -0
  59. chibi/storage/database.py +86 -0
  60. chibi/storage/dynamodb.py +257 -0
  61. chibi/storage/local.py +70 -0
  62. chibi/storage/redis.py +91 -0
  63. chibi/utils/__init__.py +0 -0
  64. chibi/utils/app.py +249 -0
  65. chibi/utils/telegram.py +521 -0
  66. chibi_bot-1.6.0b0.dist-info/LICENSE +21 -0
  67. chibi_bot-1.6.0b0.dist-info/METADATA +340 -0
  68. chibi_bot-1.6.0b0.dist-info/RECORD +70 -0
  69. chibi_bot-1.6.0b0.dist-info/WHEEL +4 -0
  70. chibi_bot-1.6.0b0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,521 @@
1
+ from collections import deque
2
+ from io import BytesIO
3
+ from typing import Any, Callable, Coroutine, ParamSpec, Type, TypeVar, cast
4
+ from urllib.parse import parse_qs, urlparse
5
+
6
+ import httpx
7
+ import telegramify_markdown
8
+ from loguru import logger
9
+ from telegram import (
10
+ Chat as TelegramChat,
11
+ )
12
+ from telegram import (
13
+ InputMediaDocument,
14
+ InputMediaPhoto,
15
+ Update,
16
+ constants,
17
+ )
18
+ from telegram import (
19
+ Message as TelegramMessage,
20
+ )
21
+ from telegram import (
22
+ User as TelegramUser,
23
+ )
24
+ from telegram.constants import FileSizeLimit
25
+ from telegram.error import BadRequest
26
+ from telegram.ext import ContextTypes
27
+
28
+ from chibi.config import telegram_settings
29
+ from chibi.constants import (
30
+ FILE_UPLOAD_TIMEOUT,
31
+ GROUP_CHAT_TYPES,
32
+ IMAGE_UPLOAD_TIMEOUT,
33
+ MARKDOWN_TOKENS,
34
+ PERSONAL_CHAT_TYPES,
35
+ UserAction,
36
+ UserContext,
37
+ )
38
+
39
+ R = TypeVar("R")
40
+ P = ParamSpec("P")
41
+
42
+
43
+ def get_telegram_user(update: Update) -> TelegramUser:
44
+ if user := update.effective_user:
45
+ return user
46
+ raise ValueError(f"Telegram incoming update does not contain valid user data. Update ID: {update.update_id}")
47
+
48
+
49
+ def get_telegram_chat(update: Update) -> TelegramChat:
50
+ if chat := update.effective_chat:
51
+ return chat
52
+ raise ValueError(f"Telegram incoming update does not contain valid chat data. Update ID: {update.update_id}")
53
+
54
+
55
+ def user_data(update: Update) -> str:
56
+ user = get_telegram_user(update=update)
57
+ return f"{user.name} ({user.id})"
58
+
59
+
60
+ def chat_data(update: Update) -> str:
61
+ chat = get_telegram_chat(update=update)
62
+ return f"{chat.type.upper()} chat ({chat.id})"
63
+
64
+
65
+ def get_telegram_message(update: Update) -> TelegramMessage:
66
+ if message := update.effective_message:
67
+ return message
68
+ raise ValueError(f"Telegram incoming update does not contain valid message data. Update ID: {update.update_id}")
69
+
70
+
71
+ def _get_next_token(text: str, pos: int, escaped: bool) -> tuple[str | None, int]:
72
+ """Find the next Markdown token at the given position.
73
+
74
+ Checks if the text starting from `pos` begins with any known Markdown token,
75
+ unless the preceding character was an escape character '\'.
76
+
77
+ Args:
78
+ text: The string to search within.
79
+ pos: The starting position in the text to check for a token.
80
+ escaped: True if the character immediately preceding `pos` was an
81
+ escape character ('\'), False otherwise.
82
+
83
+ Returns:
84
+ A tuple containing:
85
+ - The found Markdown token (str) or None if no token is found
86
+ at the position or if it's escaped.
87
+ - The length of the found token (int), or 0 if no token is found.
88
+ """
89
+ if escaped:
90
+ return None, 0
91
+ for token in MARKDOWN_TOKENS:
92
+ if text.startswith(token, pos):
93
+ return token, len(token)
94
+ return None, 0
95
+
96
+
97
+ def split_markdown_v2(
98
+ text: str,
99
+ limit: int = constants.MessageLimit.MAX_TEXT_LENGTH,
100
+ recommended_margin: int = 400,
101
+ safety_margin: int = 50,
102
+ ) -> list[str]:
103
+ """Split a Markdown text into chunks while trying to preserve formatting.
104
+
105
+ Attempts to split the text into chunks smaller than `limit`. It tracks
106
+ opening and closing Markdown tokens using a stack. When a chunk needs to be
107
+ split, it appends the necessary closing tokens to the end of the current
108
+ chunk and prepends the corresponding opening tokens to the beginning of the
109
+ next chunk.
110
+
111
+ Splitting priority:
112
+ 1. At a newline character when the buffer size is close to the limit
113
+ (within `recommended_margin`).
114
+ 2. Anywhere when the buffer size is very close to the limit
115
+ (within `safety_margin`).
116
+
117
+ Args:
118
+ text: The Markdown string to split.
119
+ limit: The maximum desired length for each chunk. Defaults to
120
+ `constants.MessageLimit.MAX_TEXT_LENGTH`.
121
+ recommended_margin: The preferred distance from the `limit` at which
122
+ to split, ideally looking for a newline.
123
+ safety_margin: The absolute minimum distance from the `limit` at which
124
+ a split must occur, regardless of the character.
125
+
126
+ Returns:
127
+ A list of strings, where each string is a chunk of the original text,
128
+ with formatting tokens adjusted to maintain validity across chunks.
129
+ """
130
+ if len(text) <= limit:
131
+ return [text]
132
+
133
+ chunks: list[str] = []
134
+ buffer: list[str] = []
135
+ stack: deque[str] = deque()
136
+
137
+ i = 0
138
+ escaped: bool = False
139
+
140
+ while i < len(text):
141
+ token, shift = _get_next_token(text=text, pos=i, escaped=escaped)
142
+ escaped = text[i] == "\\"
143
+ if token:
144
+ buffer.append(token)
145
+ if stack and stack[-1] == token:
146
+ stack.pop()
147
+ else:
148
+ stack.append(token)
149
+ i += shift
150
+ else:
151
+ buffer.append(text[i])
152
+ i += 1
153
+
154
+ if len("".join(buffer)) >= limit - recommended_margin:
155
+ if text[i] == "\n":
156
+ closing_sequence = "\n".join(reversed(stack))
157
+ chunks.append("".join(buffer) + closing_sequence)
158
+ buffer = list(stack)
159
+
160
+ elif len("".join(buffer)) >= limit - safety_margin:
161
+ closing_sequence = "\n".join(reversed(stack))
162
+ chunks.append("".join(buffer) + closing_sequence)
163
+ buffer = list(stack)
164
+
165
+ if buffer:
166
+ chunks.append("".join(buffer))
167
+ return chunks
168
+
169
+
170
+ async def send_message(
171
+ update: Update,
172
+ context: ContextTypes.DEFAULT_TYPE,
173
+ reply: bool = True,
174
+ **kwargs: Any,
175
+ ) -> TelegramMessage:
176
+ telegram_chat = get_telegram_chat(update=update)
177
+ telegram_message = get_telegram_message(update=update)
178
+
179
+ if reply:
180
+ return await context.bot.send_message(
181
+ chat_id=telegram_chat.id,
182
+ reply_to_message_id=telegram_message.message_id,
183
+ **kwargs,
184
+ )
185
+ return await context.bot.send_message(chat_id=telegram_chat.id, **kwargs)
186
+
187
+
188
+ async def send_long_message(
189
+ message: str,
190
+ update: Update,
191
+ context: ContextTypes.DEFAULT_TYPE,
192
+ parse_mode: str | None = None,
193
+ normalize_md: bool = True,
194
+ reply: bool = True,
195
+ ) -> None:
196
+ if normalize_md:
197
+ message = telegramify_markdown.markdownify(message)
198
+ chunks = split_markdown_v2(message)
199
+ else:
200
+ chunks = [
201
+ message[i : i + constants.MessageLimit.MAX_TEXT_LENGTH]
202
+ for i in range(0, len(message), constants.MessageLimit.MAX_TEXT_LENGTH)
203
+ ]
204
+
205
+ for chunk_number, chunk in enumerate(chunks):
206
+ await send_message(
207
+ update=update,
208
+ context=context,
209
+ text=chunk,
210
+ parse_mode=parse_mode,
211
+ reply=chunk_number == 0 if reply else False,
212
+ )
213
+
214
+
215
+ async def send_audio(
216
+ audio: bytes,
217
+ update: Update,
218
+ context: ContextTypes.DEFAULT_TYPE,
219
+ ) -> None:
220
+ telegram_chat = get_telegram_chat(update=update)
221
+ telegram_message = get_telegram_message(update=update)
222
+ await context.bot.send_chat_action(chat_id=telegram_chat.id, action=constants.ChatAction.RECORD_VOICE)
223
+
224
+ await context.bot.send_audio(
225
+ chat_id=telegram_chat.id, audio=audio, title="voice", reply_to_message_id=telegram_message.message_id
226
+ )
227
+
228
+
229
+ async def download_image(image_url: str) -> bytes:
230
+ parsed_url = urlparse(image_url)
231
+ params = parse_qs(parsed_url.query)
232
+ image_url = f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}"
233
+ response = await httpx.AsyncClient().get(url=image_url, params=params)
234
+ response.raise_for_status()
235
+ return response.content
236
+
237
+
238
+ async def send_images(
239
+ images: list[str] | list[BytesIO],
240
+ update: Update,
241
+ context: ContextTypes.DEFAULT_TYPE,
242
+ ) -> None:
243
+ telegram_chat = get_telegram_chat(update=update)
244
+ telegram_message = get_telegram_message(update=update)
245
+ await context.bot.send_chat_action(chat_id=telegram_chat.id, action=constants.ChatAction.UPLOAD_PHOTO)
246
+
247
+ if isinstance(images[0], str):
248
+ logger.info(f"Downloading {len(images)} images for {user_data(update)} via URLs...")
249
+ image_files = [await download_image(image_url=cast(str, url)) for url in images]
250
+ try:
251
+ logger.info(f"Uploading {len(images)} images to {user_data(update)} in the {chat_data(update)}")
252
+ await context.bot.send_media_group(
253
+ chat_id=telegram_chat.id,
254
+ media=[InputMediaPhoto(url) for url in image_files],
255
+ reply_to_message_id=telegram_message.message_id,
256
+ read_timeout=IMAGE_UPLOAD_TIMEOUT,
257
+ write_timeout=IMAGE_UPLOAD_TIMEOUT,
258
+ )
259
+ except Exception as e:
260
+ logger.error(
261
+ f"{user_data(update)} image generation request succeeded, but we couldn't send the image "
262
+ f"due to exception: {e}. Trying to send it via text message..."
263
+ )
264
+ image_urls = cast(list[str], images)
265
+ await send_message(
266
+ update=update,
267
+ context=context,
268
+ text="\n".join(image_urls),
269
+ disable_web_page_preview=False,
270
+ )
271
+ return None
272
+
273
+ logger.info(f"Uploading {len(images)} image(s) to {user_data(update)} in the {chat_data(update)}")
274
+ media_photos: list[BytesIO] = []
275
+ media_docs: list[BytesIO] = []
276
+
277
+ for file in images:
278
+ if not isinstance(file, BytesIO):
279
+ continue
280
+
281
+ file.seek(0, 2)
282
+ size = file.tell()
283
+ file.seek(0)
284
+ if size < FileSizeLimit.PHOTOSIZE_UPLOAD:
285
+ media_photos.append(file)
286
+ elif size < FileSizeLimit.FILESIZE_UPLOAD:
287
+ media_docs.append(file)
288
+ else:
289
+ logger.error(f"{user_data(update)} File size ({size}) exceeds file size limit, skipping it..")
290
+ continue
291
+
292
+ if media_photos:
293
+ await context.bot.send_media_group(
294
+ chat_id=telegram_chat.id,
295
+ media=[InputMediaPhoto(img) for img in media_photos],
296
+ reply_to_message_id=telegram_message.message_id,
297
+ write_timeout=IMAGE_UPLOAD_TIMEOUT,
298
+ )
299
+
300
+ if media_docs:
301
+ logger.info(f"Uploading {len(images)} image(s) as file(s) to {user_data(update)} in the {chat_data(update)}")
302
+
303
+ await context.bot.send_media_group(
304
+ chat_id=telegram_chat.id,
305
+ media=[InputMediaDocument(media=img, filename="file.jpeg") for img in media_docs],
306
+ reply_to_message_id=telegram_message.message_id,
307
+ write_timeout=FILE_UPLOAD_TIMEOUT,
308
+ )
309
+
310
+
311
+ async def send_text_file(file_content: str, file_name: str, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
312
+ telegram_chat = get_telegram_chat(update=update)
313
+ text_file = BytesIO(file_content.encode("utf-8"))
314
+ text_file.name = file_name
315
+
316
+ await context.bot.send_document(
317
+ chat_id=telegram_chat.id,
318
+ document=text_file,
319
+ filename=file_name,
320
+ )
321
+
322
+
323
+ async def send_message_in_plain_text_and_file(
324
+ message: str,
325
+ update: Update,
326
+ context: ContextTypes.DEFAULT_TYPE,
327
+ reply: bool = True,
328
+ ) -> None:
329
+ telegram_chat = get_telegram_chat(update=update)
330
+
331
+ await send_long_message(message=message, update=update, context=context, normalize_md=False, reply=reply)
332
+ file = BytesIO()
333
+ file.write(message.encode())
334
+ file.seek(0)
335
+ explain_message_text = (
336
+ "Oops! 😯It looks like your answer contains some code, but Telegram can't display it properly. "
337
+ "I'll additionally add your answer to the markdown file. 👇"
338
+ )
339
+
340
+ await send_message(update=update, context=context, text=explain_message_text, reply=False)
341
+ await context.bot.send_document(
342
+ chat_id=telegram_chat.id,
343
+ document=file,
344
+ filename="answer.md",
345
+ )
346
+
347
+
348
+ async def send_gpt_answer_message(
349
+ gpt_answer: str, update: Update, context: ContextTypes.DEFAULT_TYPE, reply: bool = True
350
+ ) -> None:
351
+ try:
352
+ await send_long_message(
353
+ message=gpt_answer,
354
+ update=update,
355
+ context=context,
356
+ parse_mode=constants.ParseMode.MARKDOWN_V2,
357
+ reply=reply,
358
+ )
359
+ except BadRequest as e:
360
+ # Trying to handle an exception connected with markdown parsing: just re-sending the message in a text mode.
361
+ logger.error(
362
+ f"{user_data(update)} got a Telegram Bad Request error in the {chat_data(update)} "
363
+ f"while receiving GPT answer: {e}. Trying to re-send it in plain text mode."
364
+ )
365
+ await send_message_in_plain_text_and_file(message=gpt_answer, update=update, context=context, reply=reply)
366
+
367
+
368
+ def current_user_action(context: ContextTypes.DEFAULT_TYPE) -> UserAction:
369
+ """Get the current action state associated with the user.
370
+
371
+ Retrieve the action stored under the `UserContext.ACTION` key in the user's
372
+ context data. If `user_data` is missing or the action is not set, it defaults
373
+ to `UserAction.NONE`.
374
+
375
+ Args:
376
+ context: The update context provided by the `python-telegram-bot` library.
377
+
378
+ Returns:
379
+ The current `UserAction` enum member associated with the user. Defaults to
380
+ `UserAction.NONE` if no action is set or `user_data` is unavailable.
381
+ """
382
+ if context.user_data is None:
383
+ return UserAction.NONE
384
+ return context.user_data.get(UserContext.ACTION, UserAction.NONE)
385
+
386
+
387
+ def set_user_action(context: ContextTypes.DEFAULT_TYPE, action: UserAction) -> None:
388
+ """Set the current action state for the user.
389
+
390
+ Store the provided `UserAction` under the `UserContext.ACTION` key in the user's
391
+ context data. If `user_data` does not exist, do nothing.
392
+
393
+ Args:
394
+ context: The update context provided by the `python-telegram-bot` library.
395
+ action: The `UserAction` enum member to set as the user's current action state.
396
+ """
397
+ if context.user_data is not None:
398
+ context.user_data[UserContext.ACTION] = action
399
+ return None
400
+
401
+
402
+ def user_interacts_with_bot(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
403
+ telegram_message = get_telegram_message(update=update)
404
+ prompt = telegram_message.text
405
+
406
+ if not prompt:
407
+ return False
408
+
409
+ if context.bot.first_name in prompt or context.bot.username in prompt:
410
+ return True
411
+
412
+ reply_message = telegram_message.reply_to_message
413
+ if not reply_message or not reply_message.from_user:
414
+ return False
415
+
416
+ return reply_message.from_user.id == context.bot.id
417
+
418
+
419
+ def get_user_context(context: ContextTypes.DEFAULT_TYPE, key: UserContext, expected_type: Type[R]) -> R | None:
420
+ """Retrieve a specific value from the user's context data.
421
+
422
+ Safely access the `user_data` dictionary associated with the current user
423
+ in the Telegram bot context and return the value associated with the given key,
424
+ cast to the expected type.
425
+
426
+ Args:
427
+ context: The update context provided by the `python-telegram-bot` library.
428
+ key: An enum member (UserContext) representing the key for the data to retrieve.
429
+ expected_type: The Python type the retrieved value is expected to conform to.
430
+ Used for casting the result.
431
+
432
+ Returns:
433
+ The value associated with the key, cast to `expected_type`, if it exists
434
+ and `user_data` is available. Otherwise, returns None.
435
+ """
436
+ if context.user_data is not None:
437
+ return cast(R, context.user_data.get(key, None))
438
+ return None
439
+
440
+
441
+ def set_user_context(context: ContextTypes.DEFAULT_TYPE, key: UserContext, value: object | None) -> None:
442
+ """Set or update a specific value in the user's context data.
443
+
444
+ Safely access the `user_data` dictionary associated with the current user
445
+ and store the provided value under the given key. If `user_data` does not
446
+ exist, do nothing.
447
+
448
+ Args:
449
+ context: The update context provided by the `python-telegram-bot` library.
450
+ key: An enum member (UserContext) representing the key under which to store the value.
451
+ value: The value to store in the user's context data. Can be any object or None.
452
+ """
453
+ if context.user_data is not None:
454
+ context.user_data[key] = value
455
+ return None
456
+
457
+
458
+ def user_is_allowed(tg_user: TelegramUser) -> bool:
459
+ if not telegram_settings.users_whitelist:
460
+ return True
461
+ return any(identifier in telegram_settings.users_whitelist for identifier in (str(tg_user.id), tg_user.username))
462
+
463
+
464
+ def group_is_allowed(tg_chat: TelegramChat) -> bool:
465
+ if not telegram_settings.groups_whitelist:
466
+ return True
467
+ return tg_chat.id in telegram_settings.groups_whitelist
468
+
469
+
470
+ def check_user_allowance(
471
+ func: Callable[P, Coroutine[Any, Any, R]],
472
+ ) -> Callable[P, Coroutine[Any, Any, R | None]]:
473
+ """Decorator controlling access to the chatbot.
474
+
475
+ This deco checks:
476
+ - if the specific user is allowed to interact with the chatbot, using ALLOW_BOTS and USERS_WHITELIST;
477
+ - if the chatbot is allowed to be in a specific group, using a GROUPS_WHITELIST.
478
+
479
+ If the specific user is disallowed to interact with the chatbot, the corresponding message will be sent.
480
+ If the chatbot is disallowed to be in a specific group, it will send the corresponding message
481
+ and leave it immediately.
482
+
483
+ Args:
484
+ func: async function that may rise openai exception.
485
+
486
+ Returns:
487
+ Wrapper function object.
488
+ """
489
+
490
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None:
491
+ update: Update = cast(Update, kwargs.get("update", None) or args[1])
492
+ context: ContextTypes.DEFAULT_TYPE = cast(ContextTypes.DEFAULT_TYPE, kwargs.get("context") or args[2])
493
+ telegram_chat = get_telegram_chat(update=update)
494
+ telegram_user = get_telegram_user(update=update)
495
+
496
+ if telegram_user.is_bot and not telegram_settings.allow_bots:
497
+ logger.warning(f"Bots are not allowed. Request from {user_data(update)} was ignored.")
498
+ return None
499
+
500
+ if telegram_chat.type in PERSONAL_CHAT_TYPES and not user_is_allowed(tg_user=telegram_user):
501
+ logger.warning(f"{user_data(update)} is not allowed to work with me. Request rejected.")
502
+ await send_message(
503
+ update=update,
504
+ context=context,
505
+ text=telegram_settings.message_for_disallowed_users,
506
+ )
507
+ return None
508
+
509
+ if telegram_chat.type in GROUP_CHAT_TYPES and not group_is_allowed(tg_chat=telegram_chat):
510
+ message = (
511
+ f"The group {telegram_chat.effective_name} (id: {telegram_chat.id}, link: {telegram_chat.link}) "
512
+ f"does not exist in the whitelist. Leaving it..."
513
+ )
514
+ logger.warning(message)
515
+ await context.bot.send_message(chat_id=telegram_chat.id, text=message, disable_web_page_preview=True)
516
+ await telegram_chat.leave()
517
+ return None
518
+
519
+ return await func(*args, **kwargs)
520
+
521
+ return wrapper
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Sergei Nagaev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.