bot-framework 0.1.3__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 (130) hide show
  1. bot_framework/__init__.py +57 -0
  2. bot_framework/base_protocols/__init__.py +21 -0
  3. bot_framework/base_protocols/create.py +9 -0
  4. bot_framework/base_protocols/delete.py +9 -0
  5. bot_framework/base_protocols/get_all.py +9 -0
  6. bot_framework/base_protocols/get_by_key.py +9 -0
  7. bot_framework/base_protocols/get_by_name.py +9 -0
  8. bot_framework/base_protocols/read.py +11 -0
  9. bot_framework/base_protocols/read_sequence_by_user_id.py +11 -0
  10. bot_framework/base_protocols/update.py +9 -0
  11. bot_framework/entities/__init__.py +24 -0
  12. bot_framework/entities/bot_callback.py +21 -0
  13. bot_framework/entities/bot_message.py +27 -0
  14. bot_framework/entities/bot_user.py +22 -0
  15. bot_framework/entities/button.py +8 -0
  16. bot_framework/entities/keyboard.py +9 -0
  17. bot_framework/entities/language_code.py +8 -0
  18. bot_framework/entities/parse_mode.py +6 -0
  19. bot_framework/entities/role.py +16 -0
  20. bot_framework/entities/role_name.py +7 -0
  21. bot_framework/entities/user.py +23 -0
  22. bot_framework/flow_management/__init__.py +31 -0
  23. bot_framework/flow_management/entities/__init__.py +5 -0
  24. bot_framework/flow_management/entities/flow_stack_entry.py +10 -0
  25. bot_framework/flow_management/flow_registry.py +14 -0
  26. bot_framework/flow_management/protocols/__init__.py +11 -0
  27. bot_framework/flow_management/protocols/i_flow_message_deleter.py +8 -0
  28. bot_framework/flow_management/protocols/i_flow_message_storage.py +12 -0
  29. bot_framework/flow_management/protocols/i_flow_stack_storage.py +14 -0
  30. bot_framework/flow_management/protocols/i_flow_stack_validator.py +6 -0
  31. bot_framework/flow_management/repos/__init__.py +8 -0
  32. bot_framework/flow_management/repos/redis_flow_message_storage.py +35 -0
  33. bot_framework/flow_management/repos/redis_flow_stack_storage.py +53 -0
  34. bot_framework/flow_management/services/__init__.py +15 -0
  35. bot_framework/flow_management/services/flow_message_deleter.py +33 -0
  36. bot_framework/flow_management/services/flow_stack_navigator.py +104 -0
  37. bot_framework/flow_management/services/flow_stack_validator.py +46 -0
  38. bot_framework/flows/__init__.py +11 -0
  39. bot_framework/flows/request_role_flow/__init__.py +11 -0
  40. bot_framework/flows/request_role_flow/actions/__init__.py +13 -0
  41. bot_framework/flows/request_role_flow/actions/role_assigner.py +30 -0
  42. bot_framework/flows/request_role_flow/actions/role_rejection_notifier.py +24 -0
  43. bot_framework/flows/request_role_flow/actions/role_request_sender.py +74 -0
  44. bot_framework/flows/request_role_flow/entities/__init__.py +7 -0
  45. bot_framework/flows/request_role_flow/entities/request_role_flow_state.py +6 -0
  46. bot_framework/flows/request_role_flow/exceptions.py +6 -0
  47. bot_framework/flows/request_role_flow/factory.py +146 -0
  48. bot_framework/flows/request_role_flow/handlers/__init__.py +23 -0
  49. bot_framework/flows/request_role_flow/handlers/approve_role_handler.py +48 -0
  50. bot_framework/flows/request_role_flow/handlers/reject_role_handler.py +46 -0
  51. bot_framework/flows/request_role_flow/handlers/request_role_command_handler.py +24 -0
  52. bot_framework/flows/request_role_flow/handlers/role_selection_handler.py +76 -0
  53. bot_framework/flows/request_role_flow/handlers/show_roles_handler.py +30 -0
  54. bot_framework/flows/request_role_flow/presenters/__init__.py +7 -0
  55. bot_framework/flows/request_role_flow/presenters/role_list_presenter.py +53 -0
  56. bot_framework/flows/request_role_flow/protocols/__init__.py +27 -0
  57. bot_framework/flows/request_role_flow/protocols/i_request_role_flow_router.py +7 -0
  58. bot_framework/flows/request_role_flow/protocols/i_request_role_flow_state_storage.py +11 -0
  59. bot_framework/flows/request_role_flow/protocols/i_role_assigner.py +10 -0
  60. bot_framework/flows/request_role_flow/protocols/i_role_list_presenter.py +5 -0
  61. bot_framework/flows/request_role_flow/protocols/i_role_rejection_notifier.py +9 -0
  62. bot_framework/flows/request_role_flow/protocols/i_role_request_sender.py +12 -0
  63. bot_framework/flows/request_role_flow/repos/__init__.py +7 -0
  64. bot_framework/flows/request_role_flow/repos/redis_request_role_flow_state_storage.py +33 -0
  65. bot_framework/flows/request_role_flow/request_role_flow_router.py +18 -0
  66. bot_framework/language_management/__init__.py +13 -0
  67. bot_framework/language_management/entities/__init__.py +10 -0
  68. bot_framework/language_management/entities/language.py +15 -0
  69. bot_framework/language_management/entities/phrase.py +16 -0
  70. bot_framework/language_management/repos/__init__.py +7 -0
  71. bot_framework/language_management/repos/language_repo.py +67 -0
  72. bot_framework/language_management/repos/phrase_repo.py +31 -0
  73. bot_framework/language_management/repos/protocols/__init__.py +7 -0
  74. bot_framework/language_management/repos/protocols/i_language_repo.py +10 -0
  75. bot_framework/language_management/repos/protocols/i_phrase_repo.py +9 -0
  76. bot_framework/protocols/__init__.py +37 -0
  77. bot_framework/protocols/i_callback_answerer.py +10 -0
  78. bot_framework/protocols/i_callback_handler.py +16 -0
  79. bot_framework/protocols/i_callback_handler_registry.py +9 -0
  80. bot_framework/protocols/i_card_field_formatter.py +7 -0
  81. bot_framework/protocols/i_display_width_calculator.py +5 -0
  82. bot_framework/protocols/i_ensure_user_exists.py +7 -0
  83. bot_framework/protocols/i_flow_router.py +19 -0
  84. bot_framework/protocols/i_markdown_to_html_converter.py +5 -0
  85. bot_framework/protocols/i_message_deleter.py +5 -0
  86. bot_framework/protocols/i_message_handler.py +16 -0
  87. bot_framework/protocols/i_message_handler_registry.py +14 -0
  88. bot_framework/protocols/i_message_replacer.py +17 -0
  89. bot_framework/protocols/i_message_sender.py +31 -0
  90. bot_framework/protocols/i_message_service.py +16 -0
  91. bot_framework/protocols/i_next_step_handler_registrar.py +12 -0
  92. bot_framework/protocols/i_notify_replacer.py +17 -0
  93. bot_framework/protocols/i_remaining_time_formatter.py +6 -0
  94. bot_framework/role_management/__init__.py +13 -0
  95. bot_framework/role_management/entities/__init__.py +10 -0
  96. bot_framework/role_management/entities/user_role.py +13 -0
  97. bot_framework/role_management/repos/__init__.py +5 -0
  98. bot_framework/role_management/repos/protocols/__init__.py +7 -0
  99. bot_framework/role_management/repos/protocols/i_role_repo.py +30 -0
  100. bot_framework/role_management/repos/protocols/i_user_repo.py +24 -0
  101. bot_framework/role_management/repos/role_repo.py +103 -0
  102. bot_framework/role_management/services/__init__.py +1 -0
  103. bot_framework/role_management/services/protocols/__init__.py +1 -0
  104. bot_framework/services/__init__.py +9 -0
  105. bot_framework/services/card_field_formatter.py +43 -0
  106. bot_framework/services/display_width_calculator.py +45 -0
  107. bot_framework/services/remaining_time_formatter.py +31 -0
  108. bot_framework/telegram/__init__.py +56 -0
  109. bot_framework/telegram/middleware/__init__.py +4 -0
  110. bot_framework/telegram/middleware/ensure_user_middleware.py +46 -0
  111. bot_framework/telegram/middleware/i_middleware.py +7 -0
  112. bot_framework/telegram/protocols/__init__.py +36 -0
  113. bot_framework/telegram/protocols/i_markdown_escaper.py +5 -0
  114. bot_framework/telegram/services/__init__.py +29 -0
  115. bot_framework/telegram/services/callback_answerer.py +16 -0
  116. bot_framework/telegram/services/callback_handler_registry.py +43 -0
  117. bot_framework/telegram/services/close_callback_handler.py +25 -0
  118. bot_framework/telegram/services/ensure_user_exists.py +37 -0
  119. bot_framework/telegram/services/markdown_escaper.py +8 -0
  120. bot_framework/telegram/services/markdown_to_html_converter.py +16 -0
  121. bot_framework/telegram/services/message_handler_registry.py +49 -0
  122. bot_framework/telegram/services/next_step_handler_registrar.py +43 -0
  123. bot_framework/telegram/services/telegram_message_core.py +62 -0
  124. bot_framework/telegram/services/telegram_message_deleter.py +19 -0
  125. bot_framework/telegram/services/telegram_message_replacer.py +47 -0
  126. bot_framework/telegram/services/telegram_message_sender.py +73 -0
  127. bot_framework/telegram/services/telegram_notify_replacer.py +30 -0
  128. bot_framework-0.1.3.dist-info/METADATA +71 -0
  129. bot_framework-0.1.3.dist-info/RECORD +130 -0
  130. bot_framework-0.1.3.dist-info/WHEEL +4 -0
@@ -0,0 +1,29 @@
1
+ from .callback_answerer import CallbackAnswerer
2
+ from .callback_handler_registry import CallbackHandlerRegistry
3
+ from .close_callback_handler import CloseCallbackHandler
4
+ from .ensure_user_exists import EnsureUserExists
5
+ from .markdown_escaper import MarkdownEscaper
6
+ from .markdown_to_html_converter import MarkdownToHtmlConverter
7
+ from .message_handler_registry import MessageHandlerRegistry
8
+ from .next_step_handler_registrar import NextStepHandlerRegistrar
9
+ from .telegram_message_core import TelegramMessageCore
10
+ from .telegram_message_deleter import TelegramMessageDeleter
11
+ from .telegram_message_replacer import TelegramMessageReplacer
12
+ from .telegram_message_sender import TelegramMessageSender
13
+ from .telegram_notify_replacer import TelegramNotifyReplacer
14
+
15
+ __all__ = [
16
+ "CallbackAnswerer",
17
+ "CallbackHandlerRegistry",
18
+ "CloseCallbackHandler",
19
+ "EnsureUserExists",
20
+ "MarkdownEscaper",
21
+ "MarkdownToHtmlConverter",
22
+ "MessageHandlerRegistry",
23
+ "NextStepHandlerRegistrar",
24
+ "TelegramMessageCore",
25
+ "TelegramMessageDeleter",
26
+ "TelegramMessageReplacer",
27
+ "TelegramMessageSender",
28
+ "TelegramNotifyReplacer",
29
+ ]
@@ -0,0 +1,16 @@
1
+ from telebot import TeleBot
2
+
3
+
4
+ class CallbackAnswerer:
5
+ def __init__(self, bot: TeleBot):
6
+ self.bot = bot
7
+
8
+ def answer(
9
+ self,
10
+ callback_query_id: str,
11
+ text: str | None = None,
12
+ show_alert: bool = False,
13
+ ) -> None:
14
+ self.bot.answer_callback_query(
15
+ callback_query_id, text=text, show_alert=show_alert # pyright: ignore[reportArgumentType]
16
+ )
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from telebot import TeleBot
4
+ from telebot.types import CallbackQuery
5
+
6
+ from bot_framework.entities.bot_callback import BotCallback
7
+ from bot_framework.protocols.i_callback_handler import ICallbackHandler
8
+
9
+
10
+ class CallbackHandlerRegistry:
11
+ def __init__(self, bot: TeleBot):
12
+ self.bot = bot
13
+
14
+ def register(self, handler: ICallbackHandler) -> None:
15
+ def wrapper(call: CallbackQuery) -> None:
16
+ bot_callback = self._to_bot_callback(call)
17
+ handler.handle(bot_callback)
18
+
19
+ self.bot.register_callback_query_handler(
20
+ wrapper,
21
+ func=lambda call: call.data and call.data.startswith(handler.prefix),
22
+ )
23
+
24
+ def _to_bot_callback(self, call: CallbackQuery) -> BotCallback:
25
+ if not call.from_user:
26
+ raise ValueError("call.from_user is required but was None")
27
+
28
+ message_id: int | None = None
29
+ message_chat_id: int | None = None
30
+ if call.message:
31
+ message_id = call.message.message_id
32
+ message_chat_id = call.message.chat.id
33
+
34
+ bot_callback = BotCallback(
35
+ id=str(call.id),
36
+ user_id=call.from_user.id,
37
+ data=call.data,
38
+ message_id=message_id,
39
+ message_chat_id=message_chat_id,
40
+ user_language_code=call.from_user.language_code,
41
+ )
42
+ bot_callback.set_original(call)
43
+ return bot_callback
@@ -0,0 +1,25 @@
1
+ from bot_framework.entities.bot_callback import BotCallback
2
+ from bot_framework.telegram.protocols import ICallbackAnswerer, IMessageDeleter
3
+
4
+
5
+ class CloseCallbackHandler:
6
+ def __init__(
7
+ self,
8
+ callback_answerer: ICallbackAnswerer,
9
+ message_deleter: IMessageDeleter,
10
+ ) -> None:
11
+ self.callback_answerer = callback_answerer
12
+ self.message_deleter = message_deleter
13
+ self.allowed_roles: set[str] | None = None
14
+ self.prefix = "close:"
15
+
16
+ def handle(self, callback: BotCallback) -> None:
17
+ if not callback.message_id or not callback.message_chat_id:
18
+ self.callback_answerer.answer(callback.id)
19
+ return
20
+
21
+ self.message_deleter.delete(
22
+ chat_id=callback.message_chat_id,
23
+ message_id=callback.message_id,
24
+ )
25
+ self.callback_answerer.answer(callback.id)
@@ -0,0 +1,37 @@
1
+ from bot_framework.entities import RoleName, User
2
+ from bot_framework.entities.bot_user import BotUser
3
+ from bot_framework.protocols import IEnsureUserExists
4
+ from bot_framework.role_management.repos.protocols.i_role_repo import IRoleRepo
5
+ from bot_framework.role_management.repos.protocols.i_user_repo import IUserRepo
6
+
7
+
8
+ class EnsureUserExists(IEnsureUserExists):
9
+ def __init__(
10
+ self,
11
+ user_repo: IUserRepo,
12
+ role_repo: IRoleRepo,
13
+ ) -> None:
14
+ self.user_repo = user_repo
15
+ self.role_repo = role_repo
16
+
17
+ def execute(
18
+ self,
19
+ user: BotUser,
20
+ ) -> None:
21
+ existing_user = self.user_repo.find_by_id(id=user.id)
22
+
23
+ if not existing_user:
24
+ new_user = User(
25
+ id=user.id,
26
+ username=user.username,
27
+ first_name=user.first_name,
28
+ last_name=user.last_name,
29
+ language_code=user.language_code or "en",
30
+ is_bot=user.is_bot,
31
+ is_premium=user.is_premium,
32
+ )
33
+ self.user_repo.create(entity=new_user)
34
+ self.role_repo.assign_role_by_name(
35
+ user_id=user.id,
36
+ role_name=RoleName.USER,
37
+ )
@@ -0,0 +1,8 @@
1
+ import re
2
+
3
+ from bot_framework.telegram.protocols.i_markdown_escaper import IMarkdownEscaper
4
+
5
+
6
+ class MarkdownEscaper(IMarkdownEscaper):
7
+ def escape(self, text: str) -> str:
8
+ return re.sub(r"([_*\[\]()~`>#+=|{}.!-])", r"\\\1", text)
@@ -0,0 +1,16 @@
1
+ import html
2
+ import re
3
+
4
+ from bot_framework.telegram.protocols import IMarkdownToHtmlConverter
5
+
6
+
7
+ class MarkdownToHtmlConverter(IMarkdownToHtmlConverter):
8
+ def convert(self, text: str) -> str:
9
+ result = html.escape(text)
10
+ result = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', result)
11
+ result = re.sub(r'__(.+?)__', r'<u>\1</u>', result)
12
+ result = re.sub(r'\*(.+?)\*', r'<i>\1</i>', result)
13
+ result = re.sub(r'_(.+?)_', r'<i>\1</i>', result)
14
+ result = re.sub(r'~~(.+?)~~', r'<s>\1</s>', result)
15
+ result = re.sub(r'`(.+?)`', r'<code>\1</code>', result)
16
+ return result
@@ -0,0 +1,49 @@
1
+ from collections.abc import Callable
2
+
3
+ from telebot import TeleBot
4
+ from telebot.types import Message
5
+
6
+ from bot_framework.entities.bot_message import BotMessage, BotMessageUser
7
+ from bot_framework.protocols.i_message_handler import IMessageHandler
8
+
9
+
10
+ class MessageHandlerRegistry:
11
+ def __init__(self, bot: TeleBot):
12
+ self.bot = bot
13
+
14
+ def register(
15
+ self,
16
+ handler: IMessageHandler,
17
+ commands: list[str] | None = None,
18
+ content_types: list[str] | None = None,
19
+ func: Callable[[Message], bool] | None = None,
20
+ ) -> None:
21
+ def wrapper(message: Message) -> bool | None:
22
+ bot_message = self._to_bot_message(message)
23
+ return handler.handle(bot_message)
24
+
25
+ self.bot.register_message_handler(
26
+ callback=wrapper,
27
+ commands=commands,
28
+ content_types=content_types,
29
+ func=func,
30
+ )
31
+
32
+ def _to_bot_message(self, message: Message) -> BotMessage:
33
+ if not message.from_user:
34
+ raise ValueError("message.from_user is required but was None")
35
+
36
+ from_user = BotMessageUser(
37
+ id=message.from_user.id,
38
+ language_code=message.from_user.language_code,
39
+ )
40
+
41
+ bot_message = BotMessage(
42
+ chat_id=message.chat.id,
43
+ message_id=message.message_id,
44
+ user_id=message.from_user.id,
45
+ text=message.text,
46
+ from_user=from_user,
47
+ )
48
+ bot_message.set_original(message)
49
+ return bot_message
@@ -0,0 +1,43 @@
1
+ from telebot import TeleBot
2
+ from telebot.types import Message
3
+
4
+ from bot_framework.entities.bot_message import BotMessage, BotMessageUser
5
+ from bot_framework.protocols.i_message_handler import IMessageHandler
6
+
7
+
8
+ class NextStepHandlerRegistrar:
9
+ def __init__(self, bot: TeleBot):
10
+ self.bot = bot
11
+
12
+ def register(
13
+ self,
14
+ message: BotMessage,
15
+ handler: IMessageHandler,
16
+ ) -> None:
17
+ def wrapper(msg: Message) -> bool | None:
18
+ bot_msg = self._to_bot_message(msg)
19
+ return handler.handle(bot_msg)
20
+
21
+ self.bot.register_next_step_handler(
22
+ message.get_original(),
23
+ wrapper,
24
+ )
25
+
26
+ def _to_bot_message(self, message: Message) -> BotMessage:
27
+ if not message.from_user:
28
+ raise ValueError("message.from_user is required but was None")
29
+
30
+ from_user = BotMessageUser(
31
+ id=message.from_user.id,
32
+ language_code=message.from_user.language_code,
33
+ )
34
+
35
+ bot_message = BotMessage(
36
+ chat_id=message.chat.id,
37
+ message_id=message.message_id,
38
+ user_id=message.from_user.id,
39
+ text=message.text,
40
+ from_user=from_user,
41
+ )
42
+ bot_message.set_original(message)
43
+ return bot_message
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from telebot import TeleBot
4
+ from telebot.types import (
5
+ InlineKeyboardButton,
6
+ InlineKeyboardMarkup,
7
+ )
8
+
9
+ from bot_framework.entities.bot_message import BotMessage
10
+ from bot_framework.entities.keyboard import Keyboard
11
+ from bot_framework.flow_management import IFlowMessageStorage
12
+
13
+ from .markdown_escaper import MarkdownEscaper
14
+ from .markdown_to_html_converter import MarkdownToHtmlConverter
15
+
16
+
17
+ class TelegramMessageCore:
18
+ def __init__(
19
+ self,
20
+ bot: TeleBot,
21
+ flow_message_storage: IFlowMessageStorage | None = None,
22
+ ) -> None:
23
+ self.bot = bot
24
+ self.flow_message_storage = flow_message_storage
25
+ self._markdown_escaper = MarkdownEscaper()
26
+ self._markdown_to_html_converter = MarkdownToHtmlConverter()
27
+
28
+ def register_message(
29
+ self,
30
+ chat_id: int,
31
+ message_id: int,
32
+ flow_name: str | None,
33
+ ) -> None:
34
+ if flow_name and self.flow_message_storage:
35
+ self.flow_message_storage.add_message(
36
+ telegram_id=chat_id,
37
+ flow_name=flow_name,
38
+ message_id=message_id,
39
+ )
40
+
41
+ def create_bot_message(self, chat_id: int, msg: object) -> BotMessage:
42
+ bot_message = BotMessage(chat_id=chat_id, message_id=msg.message_id) # type: ignore[attr-defined]
43
+ bot_message.set_original(msg)
44
+ return bot_message
45
+
46
+ def convert_keyboard(self, keyboard: Keyboard) -> InlineKeyboardMarkup:
47
+ markup = InlineKeyboardMarkup()
48
+ for row in keyboard.rows:
49
+ buttons = [
50
+ InlineKeyboardButton(
51
+ text=button.text, callback_data=button.callback_data
52
+ )
53
+ for button in row
54
+ ]
55
+ markup.row(*buttons)
56
+ return markup
57
+
58
+ def escape_markdown(self, text: str) -> str:
59
+ return self._markdown_escaper.escape(text)
60
+
61
+ def convert_markdown_to_html(self, text: str) -> str:
62
+ return self._markdown_to_html_converter.convert(text)
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from logging import getLogger
4
+
5
+ from telebot import TeleBot
6
+
7
+ from bot_framework.protocols.i_message_deleter import IMessageDeleter
8
+
9
+
10
+ class TelegramMessageDeleter(IMessageDeleter):
11
+ def __init__(self, bot: TeleBot) -> None:
12
+ self._bot = bot
13
+ self._logger = getLogger(__name__)
14
+
15
+ def delete(self, chat_id: int, message_id: int) -> None:
16
+ try:
17
+ self._bot.delete_message(chat_id=chat_id, message_id=message_id)
18
+ except Exception as er:
19
+ self._logger.warning("Failed to delete message", exc_info=er)
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from logging import getLogger
4
+
5
+ from bot_framework.entities.bot_message import BotMessage
6
+ from bot_framework.entities.keyboard import Keyboard
7
+ from bot_framework.entities.parse_mode import ParseMode
8
+ from bot_framework.protocols.i_message_replacer import IMessageReplacer
9
+
10
+ from .telegram_message_core import TelegramMessageCore
11
+
12
+
13
+ class TelegramMessageReplacer(IMessageReplacer):
14
+ def __init__(self, core: TelegramMessageCore) -> None:
15
+ self._core = core
16
+ self._logger = getLogger(__name__)
17
+
18
+ def replace(
19
+ self,
20
+ chat_id: int,
21
+ message_id: int,
22
+ text: str,
23
+ parse_mode: ParseMode = ParseMode.HTML,
24
+ keyboard: Keyboard | None = None,
25
+ flow_name: str | None = None,
26
+ ) -> BotMessage:
27
+ reply_markup = self._core.convert_keyboard(keyboard) if keyboard else None
28
+ text_to_send = text
29
+ if parse_mode == ParseMode.MARKDOWN:
30
+ text_to_send = self._core.escape_markdown(text)
31
+ try:
32
+ msg = self._core.bot.edit_message_text(
33
+ chat_id=chat_id,
34
+ message_id=message_id,
35
+ text=text_to_send,
36
+ parse_mode=parse_mode.value,
37
+ reply_markup=reply_markup,
38
+ )
39
+ self._core.register_message(chat_id, message_id, flow_name)
40
+ return self._core.create_bot_message(chat_id, msg)
41
+ except Exception as er:
42
+ if "message is not modified" in str(er):
43
+ self._logger.debug("Message content unchanged, skipping edit")
44
+ bot_message = BotMessage(chat_id=chat_id, message_id=message_id)
45
+ return bot_message
46
+ self._logger.error("Failed to replace message", exc_info=er)
47
+ raise
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from logging import getLogger
4
+
5
+ from bot_framework.entities.bot_message import BotMessage
6
+ from bot_framework.entities.keyboard import Keyboard
7
+ from bot_framework.entities.parse_mode import ParseMode
8
+ from bot_framework.protocols.i_message_sender import IMessageSender
9
+
10
+ from .telegram_message_core import TelegramMessageCore
11
+
12
+
13
+ class TelegramMessageSender(IMessageSender):
14
+ def __init__(self, core: TelegramMessageCore) -> None:
15
+ self._core = core
16
+ self._logger = getLogger(__name__)
17
+
18
+ def send(
19
+ self,
20
+ chat_id: int,
21
+ text: str,
22
+ parse_mode: ParseMode = ParseMode.HTML,
23
+ keyboard: Keyboard | None = None,
24
+ flow_name: str | None = None,
25
+ ) -> BotMessage:
26
+ reply_markup = self._core.convert_keyboard(keyboard) if keyboard else None
27
+ text_to_send = text
28
+ if parse_mode == ParseMode.MARKDOWN:
29
+ text_to_send = self._core.escape_markdown(text)
30
+ try:
31
+ msg = self._core.bot.send_message(
32
+ chat_id=chat_id,
33
+ text=text_to_send,
34
+ parse_mode=parse_mode.value,
35
+ reply_markup=reply_markup,
36
+ )
37
+ self._core.register_message(chat_id, msg.message_id, flow_name)
38
+ return self._core.create_bot_message(chat_id, msg)
39
+ except Exception as er:
40
+ self._logger.error("Failed to send message", exc_info=er)
41
+ msg = self._core.bot.send_message(
42
+ chat_id=chat_id,
43
+ text=text,
44
+ reply_markup=reply_markup,
45
+ )
46
+ self._core.register_message(chat_id, msg.message_id, flow_name)
47
+ return self._core.create_bot_message(chat_id, msg)
48
+
49
+ def send_markdown_as_html(
50
+ self,
51
+ chat_id: int,
52
+ text: str,
53
+ keyboard: Keyboard | None = None,
54
+ flow_name: str | None = None,
55
+ ) -> BotMessage:
56
+ html_text = self._core.convert_markdown_to_html(text)
57
+ return self.send(chat_id, html_text, ParseMode.HTML, keyboard, flow_name)
58
+
59
+ def send_document(
60
+ self,
61
+ chat_id: int,
62
+ document: bytes,
63
+ filename: str,
64
+ ) -> BotMessage:
65
+ from io import BytesIO
66
+
67
+ file_obj = BytesIO(document)
68
+ file_obj.name = filename
69
+ msg = self._core.bot.send_document(
70
+ chat_id=chat_id,
71
+ document=file_obj,
72
+ )
73
+ return self._core.create_bot_message(chat_id, msg)
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from bot_framework.entities.bot_message import BotMessage
4
+ from bot_framework.entities.keyboard import Keyboard
5
+ from bot_framework.entities.parse_mode import ParseMode
6
+ from bot_framework.protocols.i_message_deleter import IMessageDeleter
7
+ from bot_framework.protocols.i_message_sender import IMessageSender
8
+ from bot_framework.protocols.i_notify_replacer import INotifyReplacer
9
+
10
+
11
+ class TelegramNotifyReplacer(INotifyReplacer):
12
+ def __init__(
13
+ self,
14
+ sender: IMessageSender,
15
+ deleter: IMessageDeleter,
16
+ ) -> None:
17
+ self._sender = sender
18
+ self._deleter = deleter
19
+
20
+ def notify_replace(
21
+ self,
22
+ chat_id: int,
23
+ message_id: int,
24
+ text: str,
25
+ parse_mode: ParseMode = ParseMode.HTML,
26
+ keyboard: Keyboard | None = None,
27
+ flow_name: str | None = None,
28
+ ) -> BotMessage:
29
+ self._deleter.delete(chat_id=chat_id, message_id=message_id)
30
+ return self._sender.send(chat_id, text, parse_mode, keyboard, flow_name)
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: bot-framework
3
+ Version: 0.1.3
4
+ Summary: Reusable Telegram bot framework with Clean Architecture
5
+ Author-email: Vladimir Sumarokov <sumarokov.vp@gmail.com>
6
+ Requires-Python: >=3.13
7
+ Requires-Dist: pydantic>=2.11.0
8
+ Provides-Extra: all
9
+ Requires-Dist: psycopg[binary]>=3.2.0; extra == 'all'
10
+ Requires-Dist: pytelegrambotapi>=4.29.0; extra == 'all'
11
+ Requires-Dist: redis>=6.0.0; extra == 'all'
12
+ Provides-Extra: postgres
13
+ Requires-Dist: psycopg[binary]>=3.2.0; extra == 'postgres'
14
+ Provides-Extra: redis
15
+ Requires-Dist: redis>=6.0.0; extra == 'redis'
16
+ Provides-Extra: telegram
17
+ Requires-Dist: pytelegrambotapi>=4.29.0; extra == 'telegram'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Bot Framework
21
+
22
+ Reusable Python library for building Telegram bots with Clean Architecture principles.
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ # Basic installation
28
+ pip install bot-framework
29
+
30
+ # With Telegram support
31
+ pip install bot-framework[telegram]
32
+
33
+ # With all optional dependencies
34
+ pip install bot-framework[all]
35
+ ```
36
+
37
+ ## Features
38
+
39
+ - **Clean Architecture** - Layered architecture with import-linter enforcement
40
+ - **Telegram Integration** - Ready-to-use services for pyTelegramBotAPI
41
+ - **Flow Management** - Dialog flow stack management with Redis storage
42
+ - **Role Management** - User roles and permissions
43
+ - **Language Management** - Multilingual phrase support
44
+ - **Request Role Flow** - Pre-built flow for role requests
45
+
46
+ ## Quick Start
47
+
48
+ ```python
49
+ from bot_framework import Button, Keyboard, IMessageSender
50
+ from bot_framework.telegram import TelegramMessageSender
51
+
52
+ # Create keyboard
53
+ keyboard = Keyboard(rows=[
54
+ [Button(text="Option 1", callback_data="opt1")],
55
+ [Button(text="Option 2", callback_data="opt2")],
56
+ ])
57
+
58
+ # Send message (implement IMessageSender or use TelegramMessageSender)
59
+ sender.send(chat_id=123, text="Choose an option:", keyboard=keyboard)
60
+ ```
61
+
62
+ ## Optional Dependencies
63
+
64
+ - `telegram` - pyTelegramBotAPI for Telegram bot integration
65
+ - `postgres` - psycopg for PostgreSQL database support
66
+ - `redis` - Redis for caching and flow state management
67
+ - `all` - All optional dependencies
68
+
69
+ ## License
70
+
71
+ MIT