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.
- bot_framework/__init__.py +57 -0
- bot_framework/base_protocols/__init__.py +21 -0
- bot_framework/base_protocols/create.py +9 -0
- bot_framework/base_protocols/delete.py +9 -0
- bot_framework/base_protocols/get_all.py +9 -0
- bot_framework/base_protocols/get_by_key.py +9 -0
- bot_framework/base_protocols/get_by_name.py +9 -0
- bot_framework/base_protocols/read.py +11 -0
- bot_framework/base_protocols/read_sequence_by_user_id.py +11 -0
- bot_framework/base_protocols/update.py +9 -0
- bot_framework/entities/__init__.py +24 -0
- bot_framework/entities/bot_callback.py +21 -0
- bot_framework/entities/bot_message.py +27 -0
- bot_framework/entities/bot_user.py +22 -0
- bot_framework/entities/button.py +8 -0
- bot_framework/entities/keyboard.py +9 -0
- bot_framework/entities/language_code.py +8 -0
- bot_framework/entities/parse_mode.py +6 -0
- bot_framework/entities/role.py +16 -0
- bot_framework/entities/role_name.py +7 -0
- bot_framework/entities/user.py +23 -0
- bot_framework/flow_management/__init__.py +31 -0
- bot_framework/flow_management/entities/__init__.py +5 -0
- bot_framework/flow_management/entities/flow_stack_entry.py +10 -0
- bot_framework/flow_management/flow_registry.py +14 -0
- bot_framework/flow_management/protocols/__init__.py +11 -0
- bot_framework/flow_management/protocols/i_flow_message_deleter.py +8 -0
- bot_framework/flow_management/protocols/i_flow_message_storage.py +12 -0
- bot_framework/flow_management/protocols/i_flow_stack_storage.py +14 -0
- bot_framework/flow_management/protocols/i_flow_stack_validator.py +6 -0
- bot_framework/flow_management/repos/__init__.py +8 -0
- bot_framework/flow_management/repos/redis_flow_message_storage.py +35 -0
- bot_framework/flow_management/repos/redis_flow_stack_storage.py +53 -0
- bot_framework/flow_management/services/__init__.py +15 -0
- bot_framework/flow_management/services/flow_message_deleter.py +33 -0
- bot_framework/flow_management/services/flow_stack_navigator.py +104 -0
- bot_framework/flow_management/services/flow_stack_validator.py +46 -0
- bot_framework/flows/__init__.py +11 -0
- bot_framework/flows/request_role_flow/__init__.py +11 -0
- bot_framework/flows/request_role_flow/actions/__init__.py +13 -0
- bot_framework/flows/request_role_flow/actions/role_assigner.py +30 -0
- bot_framework/flows/request_role_flow/actions/role_rejection_notifier.py +24 -0
- bot_framework/flows/request_role_flow/actions/role_request_sender.py +74 -0
- bot_framework/flows/request_role_flow/entities/__init__.py +7 -0
- bot_framework/flows/request_role_flow/entities/request_role_flow_state.py +6 -0
- bot_framework/flows/request_role_flow/exceptions.py +6 -0
- bot_framework/flows/request_role_flow/factory.py +146 -0
- bot_framework/flows/request_role_flow/handlers/__init__.py +23 -0
- bot_framework/flows/request_role_flow/handlers/approve_role_handler.py +48 -0
- bot_framework/flows/request_role_flow/handlers/reject_role_handler.py +46 -0
- bot_framework/flows/request_role_flow/handlers/request_role_command_handler.py +24 -0
- bot_framework/flows/request_role_flow/handlers/role_selection_handler.py +76 -0
- bot_framework/flows/request_role_flow/handlers/show_roles_handler.py +30 -0
- bot_framework/flows/request_role_flow/presenters/__init__.py +7 -0
- bot_framework/flows/request_role_flow/presenters/role_list_presenter.py +53 -0
- bot_framework/flows/request_role_flow/protocols/__init__.py +27 -0
- bot_framework/flows/request_role_flow/protocols/i_request_role_flow_router.py +7 -0
- bot_framework/flows/request_role_flow/protocols/i_request_role_flow_state_storage.py +11 -0
- bot_framework/flows/request_role_flow/protocols/i_role_assigner.py +10 -0
- bot_framework/flows/request_role_flow/protocols/i_role_list_presenter.py +5 -0
- bot_framework/flows/request_role_flow/protocols/i_role_rejection_notifier.py +9 -0
- bot_framework/flows/request_role_flow/protocols/i_role_request_sender.py +12 -0
- bot_framework/flows/request_role_flow/repos/__init__.py +7 -0
- bot_framework/flows/request_role_flow/repos/redis_request_role_flow_state_storage.py +33 -0
- bot_framework/flows/request_role_flow/request_role_flow_router.py +18 -0
- bot_framework/language_management/__init__.py +13 -0
- bot_framework/language_management/entities/__init__.py +10 -0
- bot_framework/language_management/entities/language.py +15 -0
- bot_framework/language_management/entities/phrase.py +16 -0
- bot_framework/language_management/repos/__init__.py +7 -0
- bot_framework/language_management/repos/language_repo.py +67 -0
- bot_framework/language_management/repos/phrase_repo.py +31 -0
- bot_framework/language_management/repos/protocols/__init__.py +7 -0
- bot_framework/language_management/repos/protocols/i_language_repo.py +10 -0
- bot_framework/language_management/repos/protocols/i_phrase_repo.py +9 -0
- bot_framework/protocols/__init__.py +37 -0
- bot_framework/protocols/i_callback_answerer.py +10 -0
- bot_framework/protocols/i_callback_handler.py +16 -0
- bot_framework/protocols/i_callback_handler_registry.py +9 -0
- bot_framework/protocols/i_card_field_formatter.py +7 -0
- bot_framework/protocols/i_display_width_calculator.py +5 -0
- bot_framework/protocols/i_ensure_user_exists.py +7 -0
- bot_framework/protocols/i_flow_router.py +19 -0
- bot_framework/protocols/i_markdown_to_html_converter.py +5 -0
- bot_framework/protocols/i_message_deleter.py +5 -0
- bot_framework/protocols/i_message_handler.py +16 -0
- bot_framework/protocols/i_message_handler_registry.py +14 -0
- bot_framework/protocols/i_message_replacer.py +17 -0
- bot_framework/protocols/i_message_sender.py +31 -0
- bot_framework/protocols/i_message_service.py +16 -0
- bot_framework/protocols/i_next_step_handler_registrar.py +12 -0
- bot_framework/protocols/i_notify_replacer.py +17 -0
- bot_framework/protocols/i_remaining_time_formatter.py +6 -0
- bot_framework/role_management/__init__.py +13 -0
- bot_framework/role_management/entities/__init__.py +10 -0
- bot_framework/role_management/entities/user_role.py +13 -0
- bot_framework/role_management/repos/__init__.py +5 -0
- bot_framework/role_management/repos/protocols/__init__.py +7 -0
- bot_framework/role_management/repos/protocols/i_role_repo.py +30 -0
- bot_framework/role_management/repos/protocols/i_user_repo.py +24 -0
- bot_framework/role_management/repos/role_repo.py +103 -0
- bot_framework/role_management/services/__init__.py +1 -0
- bot_framework/role_management/services/protocols/__init__.py +1 -0
- bot_framework/services/__init__.py +9 -0
- bot_framework/services/card_field_formatter.py +43 -0
- bot_framework/services/display_width_calculator.py +45 -0
- bot_framework/services/remaining_time_formatter.py +31 -0
- bot_framework/telegram/__init__.py +56 -0
- bot_framework/telegram/middleware/__init__.py +4 -0
- bot_framework/telegram/middleware/ensure_user_middleware.py +46 -0
- bot_framework/telegram/middleware/i_middleware.py +7 -0
- bot_framework/telegram/protocols/__init__.py +36 -0
- bot_framework/telegram/protocols/i_markdown_escaper.py +5 -0
- bot_framework/telegram/services/__init__.py +29 -0
- bot_framework/telegram/services/callback_answerer.py +16 -0
- bot_framework/telegram/services/callback_handler_registry.py +43 -0
- bot_framework/telegram/services/close_callback_handler.py +25 -0
- bot_framework/telegram/services/ensure_user_exists.py +37 -0
- bot_framework/telegram/services/markdown_escaper.py +8 -0
- bot_framework/telegram/services/markdown_to_html_converter.py +16 -0
- bot_framework/telegram/services/message_handler_registry.py +49 -0
- bot_framework/telegram/services/next_step_handler_registrar.py +43 -0
- bot_framework/telegram/services/telegram_message_core.py +62 -0
- bot_framework/telegram/services/telegram_message_deleter.py +19 -0
- bot_framework/telegram/services/telegram_message_replacer.py +47 -0
- bot_framework/telegram/services/telegram_message_sender.py +73 -0
- bot_framework/telegram/services/telegram_notify_replacer.py +30 -0
- bot_framework-0.1.3.dist-info/METADATA +71 -0
- bot_framework-0.1.3.dist-info/RECORD +130 -0
- 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,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
|