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,104 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from datetime import UTC, datetime
5
+
6
+ from bot_framework.entities.user import User
7
+ from bot_framework.flow_management.entities import FlowStackEntry
8
+ from bot_framework.flow_management.flow_registry import FlowRegistry
9
+ from bot_framework.flow_management.protocols import (
10
+ IFlowMessageDeleter,
11
+ IFlowStackStorage,
12
+ IFlowStackValidator,
13
+ )
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class FlowStackNavigator:
19
+ def __init__(
20
+ self,
21
+ storage: IFlowStackStorage,
22
+ registry: FlowRegistry,
23
+ validator: IFlowStackValidator,
24
+ message_deleter: IFlowMessageDeleter | None = None,
25
+ ):
26
+ self._storage = storage
27
+ self._registry = registry
28
+ self._validator = validator
29
+ self._message_deleter = message_deleter
30
+
31
+ def push(self, user: User, flow_name: str) -> None:
32
+ self._validator.validate_push(user.id, flow_name)
33
+
34
+ stack = self._storage.get_stack(user.id)
35
+ entry = FlowStackEntry(flow_name=flow_name, started_at=datetime.now(UTC))
36
+ self._storage.push(user.id, entry)
37
+
38
+ logger.info(
39
+ f"Flow pushed for user {user.id}: {flow_name}. "
40
+ f"Stack depth: {len(stack) + 1}"
41
+ )
42
+
43
+ def pop_and_return(self, user: User) -> None:
44
+ current_entry = self._storage.pop(user.id)
45
+ if not current_entry:
46
+ logger.warning(f"No flow to pop for user {user.id}")
47
+ return
48
+
49
+ self._clean_flow_messages(user.id, current_entry.flow_name)
50
+
51
+ logger.info(
52
+ f"Flow popped for user {user.id}: {current_entry.flow_name}. "
53
+ f"Duration: {datetime.now(UTC) - current_entry.started_at}"
54
+ )
55
+
56
+ remaining_stack = self._storage.get_stack(user.id)
57
+ if not remaining_stack:
58
+ logger.info(f"Flow stack empty for user {user.id}")
59
+ return
60
+
61
+ parent_entry = remaining_stack[-1]
62
+ parent_flow = self._registry.get(parent_entry.flow_name)
63
+ if not parent_flow:
64
+ logger.error(
65
+ f"Parent flow '{parent_entry.flow_name}' not found in registry"
66
+ )
67
+ return
68
+
69
+ logger.info(
70
+ f"Returning to parent flow for user {user.id}: {parent_entry.flow_name}"
71
+ )
72
+ parent_flow.start(user)
73
+
74
+ def terminate(self, user: User) -> None:
75
+ current_entry = self._storage.pop(user.id)
76
+ if not current_entry:
77
+ logger.warning(f"No flow to terminate for user {user.id}")
78
+ return
79
+
80
+ self._clean_flow_messages(user.id, current_entry.flow_name)
81
+
82
+ logger.info(
83
+ f"Flow terminated for user {user.id}: {current_entry.flow_name}. "
84
+ f"Duration: {datetime.now(UTC) - current_entry.started_at}"
85
+ )
86
+
87
+ def is_flow_in_stack(self, user: User, flow_name: str) -> bool:
88
+ stack = self._storage.get_stack(user.id)
89
+ return flow_name in [entry.flow_name for entry in stack]
90
+
91
+ def clear_all(self, user: User) -> None:
92
+ stack = self._storage.get_stack(user.id)
93
+
94
+ if self._message_deleter and stack:
95
+ for entry in stack:
96
+ self._message_deleter.delete_flow_messages(user.id, entry.flow_name)
97
+
98
+ if stack:
99
+ logger.info(f"Clearing flow stack for user {user.id}. Depth: {len(stack)}")
100
+ self._storage.clear(user.id)
101
+
102
+ def _clean_flow_messages(self, chat_id: int, flow_name: str) -> None:
103
+ if self._message_deleter:
104
+ self._message_deleter.delete_flow_messages(chat_id, flow_name)
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ from bot_framework.flow_management.flow_registry import FlowRegistry
6
+ from bot_framework.flow_management.protocols import IFlowStackStorage
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class FlowStackValidator:
12
+ MAX_STACK_DEPTH = 5
13
+
14
+ def __init__(
15
+ self,
16
+ storage: IFlowStackStorage,
17
+ registry: FlowRegistry,
18
+ ):
19
+ self._storage = storage
20
+ self._registry = registry
21
+
22
+ def validate_push(self, user_id: int, flow_name: str) -> None:
23
+ stack = self._storage.get_stack(user_id)
24
+
25
+ if len(stack) >= self.MAX_STACK_DEPTH:
26
+ logger.error(
27
+ f"Flow stack depth exceeded for user {user_id}. "
28
+ f"Max depth: {self.MAX_STACK_DEPTH}"
29
+ )
30
+ raise ValueError(
31
+ f"Maximum flow stack depth ({self.MAX_STACK_DEPTH}) exceeded"
32
+ )
33
+
34
+ flow_names_in_stack = [entry.flow_name for entry in stack]
35
+ if flow_name in flow_names_in_stack:
36
+ logger.error(
37
+ f"Circular flow dependency detected for user {user_id}: "
38
+ f"{flow_names_in_stack} -> {flow_name}"
39
+ )
40
+ raise ValueError(
41
+ f"Circular flow dependency detected: {flow_name} is already in stack"
42
+ )
43
+
44
+ if not self._registry.get(flow_name):
45
+ logger.error(f"Flow '{flow_name}' not found in registry")
46
+ raise ValueError(f"Flow '{flow_name}' not registered")
@@ -0,0 +1,11 @@
1
+ from bot_framework.flows.request_role_flow import (
2
+ IRequestRoleFlowRouter,
3
+ RequestRoleFlowFactory,
4
+ RequestRoleFlowRouter,
5
+ )
6
+
7
+ __all__ = [
8
+ "IRequestRoleFlowRouter",
9
+ "RequestRoleFlowFactory",
10
+ "RequestRoleFlowRouter",
11
+ ]
@@ -0,0 +1,11 @@
1
+ from bot_framework.flows.request_role_flow.factory import RequestRoleFlowFactory
2
+ from bot_framework.flows.request_role_flow.protocols import IRequestRoleFlowRouter
3
+ from bot_framework.flows.request_role_flow.request_role_flow_router import (
4
+ RequestRoleFlowRouter,
5
+ )
6
+
7
+ __all__ = [
8
+ "IRequestRoleFlowRouter",
9
+ "RequestRoleFlowFactory",
10
+ "RequestRoleFlowRouter",
11
+ ]
@@ -0,0 +1,13 @@
1
+ from bot_framework.flows.request_role_flow.actions.role_assigner import RoleAssigner
2
+ from bot_framework.flows.request_role_flow.actions.role_rejection_notifier import (
3
+ RoleRejectionNotifier,
4
+ )
5
+ from bot_framework.flows.request_role_flow.actions.role_request_sender import (
6
+ RoleRequestSender,
7
+ )
8
+
9
+ __all__ = [
10
+ "RoleAssigner",
11
+ "RoleRejectionNotifier",
12
+ "RoleRequestSender",
13
+ ]
@@ -0,0 +1,30 @@
1
+ from bot_framework.language_management.repos.protocols.i_phrase_repo import IPhraseRepo
2
+ from bot_framework.protocols.i_message_sender import IMessageSender
3
+ from bot_framework.role_management.repos.protocols.i_role_repo import IRoleRepo
4
+
5
+
6
+ class RoleAssigner:
7
+ def __init__(
8
+ self,
9
+ message_sender: IMessageSender,
10
+ phrase_repo: IPhraseRepo,
11
+ role_repo: IRoleRepo,
12
+ ) -> None:
13
+ self.message_sender = message_sender
14
+ self.phrase_repo = phrase_repo
15
+ self.role_repo = role_repo
16
+
17
+ def assign_and_notify(
18
+ self,
19
+ user_id: int,
20
+ role_id: int,
21
+ language_code: str,
22
+ ) -> None:
23
+ self.role_repo.assign_role(user_id=user_id, role_id=role_id)
24
+
25
+ text = self.phrase_repo.get_phrase(
26
+ key="request_role.approved",
27
+ language_code=language_code,
28
+ )
29
+
30
+ self.message_sender.send(chat_id=user_id, text=text)
@@ -0,0 +1,24 @@
1
+ from bot_framework.language_management.repos.protocols.i_phrase_repo import IPhraseRepo
2
+ from bot_framework.protocols.i_message_sender import IMessageSender
3
+
4
+
5
+ class RoleRejectionNotifier:
6
+ def __init__(
7
+ self,
8
+ message_sender: IMessageSender,
9
+ phrase_repo: IPhraseRepo,
10
+ ) -> None:
11
+ self.message_sender = message_sender
12
+ self.phrase_repo = phrase_repo
13
+
14
+ def notify(
15
+ self,
16
+ user_id: int,
17
+ language_code: str,
18
+ ) -> None:
19
+ text = self.phrase_repo.get_phrase(
20
+ key="request_role.rejected",
21
+ language_code=language_code,
22
+ )
23
+
24
+ self.message_sender.send(chat_id=user_id, text=text)
@@ -0,0 +1,74 @@
1
+ from bot_framework.entities.button import Button
2
+ from bot_framework.entities.keyboard import Keyboard
3
+ from bot_framework.entities.role import Role
4
+ from bot_framework.entities.user import User
5
+ from bot_framework.flows.request_role_flow.exceptions import NoSupervisorsFoundError
6
+ from bot_framework.language_management.repos.protocols.i_phrase_repo import IPhraseRepo
7
+ from bot_framework.protocols.i_message_sender import IMessageSender
8
+ from bot_framework.role_management.repos.protocols.i_user_repo import IUserRepo
9
+
10
+
11
+ class RoleRequestSender:
12
+ def __init__(
13
+ self,
14
+ message_sender: IMessageSender,
15
+ phrase_repo: IPhraseRepo,
16
+ user_repo: IUserRepo,
17
+ approve_handler_prefix: str,
18
+ reject_handler_prefix: str,
19
+ ) -> None:
20
+ self.message_sender = message_sender
21
+ self.phrase_repo = phrase_repo
22
+ self.user_repo = user_repo
23
+ self.approve_handler_prefix = approve_handler_prefix
24
+ self.reject_handler_prefix = reject_handler_prefix
25
+
26
+ def send_to_supervisors(
27
+ self,
28
+ requester: User,
29
+ role: Role,
30
+ ) -> None:
31
+ supervisors = self.user_repo.get_by_role_name(role_name="supervisors")
32
+ if not supervisors:
33
+ raise NoSupervisorsFoundError()
34
+
35
+ for supervisor in supervisors:
36
+ language_code = supervisor.language_code
37
+
38
+ title = self.phrase_repo.get_phrase(
39
+ key="request_role.supervisor.title",
40
+ language_code=language_code,
41
+ )
42
+
43
+ requester_name = requester.username or requester.first_name or str(requester.id)
44
+ text = f"{title}\n\nUser: {requester_name}\nRole: {role.name}"
45
+
46
+ approve_text = self.phrase_repo.get_phrase(
47
+ key="request_role.button.approve",
48
+ language_code=language_code,
49
+ )
50
+ reject_text = self.phrase_repo.get_phrase(
51
+ key="request_role.button.reject",
52
+ language_code=language_code,
53
+ )
54
+
55
+ keyboard = Keyboard(
56
+ rows=[
57
+ [
58
+ Button(
59
+ text=approve_text,
60
+ callback_data=f"{self.approve_handler_prefix}:{role.id}:{requester.id}",
61
+ ),
62
+ Button(
63
+ text=reject_text,
64
+ callback_data=f"{self.reject_handler_prefix}:{role.id}:{requester.id}",
65
+ ),
66
+ ]
67
+ ]
68
+ )
69
+
70
+ self.message_sender.send(
71
+ chat_id=supervisor.id,
72
+ text=text,
73
+ keyboard=keyboard,
74
+ )
@@ -0,0 +1,7 @@
1
+ from bot_framework.flows.request_role_flow.entities.request_role_flow_state import (
2
+ RequestRoleFlowState,
3
+ )
4
+
5
+ __all__ = [
6
+ "RequestRoleFlowState",
7
+ ]
@@ -0,0 +1,6 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class RequestRoleFlowState(BaseModel):
5
+ requester_user_id: int
6
+ selected_role_id: int | None = None
@@ -0,0 +1,6 @@
1
+ class NoSupervisorsFoundError(Exception):
2
+ pass
3
+
4
+
5
+ class UserAlreadyHasRoleError(Exception):
6
+ pass
@@ -0,0 +1,146 @@
1
+ from bot_framework.flows.request_role_flow.actions import (
2
+ RoleAssigner,
3
+ RoleRejectionNotifier,
4
+ RoleRequestSender,
5
+ )
6
+ from bot_framework.flows.request_role_flow.handlers import (
7
+ ApproveRoleHandler,
8
+ RejectRoleHandler,
9
+ RequestRoleCommandHandler,
10
+ RoleSelectionHandler,
11
+ ShowRolesHandler,
12
+ )
13
+ from bot_framework.flows.request_role_flow.presenters import RoleListPresenter
14
+ from bot_framework.flows.request_role_flow.protocols import (
15
+ IRequestRoleFlowRouter,
16
+ IRequestRoleFlowStateStorage,
17
+ )
18
+ from bot_framework.flows.request_role_flow.request_role_flow_router import (
19
+ RequestRoleFlowRouter,
20
+ )
21
+ from bot_framework.language_management.repos.protocols.i_phrase_repo import IPhraseRepo
22
+ from bot_framework.protocols.i_callback_answerer import ICallbackAnswerer
23
+ from bot_framework.protocols.i_callback_handler_registry import (
24
+ ICallbackHandlerRegistry,
25
+ )
26
+ from bot_framework.protocols.i_message_handler_registry import IMessageHandlerRegistry
27
+ from bot_framework.protocols.i_message_sender import IMessageSender
28
+ from bot_framework.role_management.repos.protocols.i_role_repo import IRoleRepo
29
+ from bot_framework.role_management.repos.protocols.i_user_repo import IUserRepo
30
+
31
+
32
+ class RequestRoleFlowFactory:
33
+ def __init__(
34
+ self,
35
+ callback_answerer: ICallbackAnswerer,
36
+ message_sender: IMessageSender,
37
+ phrase_repo: IPhraseRepo,
38
+ role_repo: IRoleRepo,
39
+ user_repo: IUserRepo,
40
+ state_storage: IRequestRoleFlowStateStorage,
41
+ ) -> None:
42
+ self.callback_answerer = callback_answerer
43
+ self.message_sender = message_sender
44
+ self.phrase_repo = phrase_repo
45
+ self.role_repo = role_repo
46
+ self.user_repo = user_repo
47
+ self.state_storage = state_storage
48
+
49
+ self._show_roles_handler: ShowRolesHandler | None = None
50
+ self._role_selection_handler: RoleSelectionHandler | None = None
51
+ self._approve_handler: ApproveRoleHandler | None = None
52
+ self._reject_handler: RejectRoleHandler | None = None
53
+
54
+ def _get_approve_handler(self) -> ApproveRoleHandler:
55
+ if self._approve_handler is None:
56
+ self._approve_handler = ApproveRoleHandler(
57
+ callback_answerer=self.callback_answerer,
58
+ role_repo=self.role_repo,
59
+ user_repo=self.user_repo,
60
+ role_assigner=RoleAssigner(
61
+ message_sender=self.message_sender,
62
+ phrase_repo=self.phrase_repo,
63
+ role_repo=self.role_repo,
64
+ ),
65
+ )
66
+ return self._approve_handler
67
+
68
+ def _get_reject_handler(self) -> RejectRoleHandler:
69
+ if self._reject_handler is None:
70
+ self._reject_handler = RejectRoleHandler(
71
+ callback_answerer=self.callback_answerer,
72
+ role_repo=self.role_repo,
73
+ user_repo=self.user_repo,
74
+ role_rejection_notifier=RoleRejectionNotifier(
75
+ message_sender=self.message_sender,
76
+ phrase_repo=self.phrase_repo,
77
+ ),
78
+ )
79
+ return self._reject_handler
80
+
81
+ def _get_role_selection_handler(self) -> RoleSelectionHandler:
82
+ if self._role_selection_handler is None:
83
+ approve_handler = self._get_approve_handler()
84
+ reject_handler = self._get_reject_handler()
85
+
86
+ self._role_selection_handler = RoleSelectionHandler(
87
+ callback_answerer=self.callback_answerer,
88
+ message_sender=self.message_sender,
89
+ phrase_repo=self.phrase_repo,
90
+ role_repo=self.role_repo,
91
+ user_repo=self.user_repo,
92
+ state_storage=self.state_storage,
93
+ role_request_sender=RoleRequestSender(
94
+ message_sender=self.message_sender,
95
+ phrase_repo=self.phrase_repo,
96
+ user_repo=self.user_repo,
97
+ approve_handler_prefix=approve_handler.prefix,
98
+ reject_handler_prefix=reject_handler.prefix,
99
+ ),
100
+ )
101
+ return self._role_selection_handler
102
+
103
+ def _get_show_roles_handler(self) -> ShowRolesHandler:
104
+ if self._show_roles_handler is None:
105
+ role_selection_handler = self._get_role_selection_handler()
106
+
107
+ self._show_roles_handler = ShowRolesHandler(
108
+ callback_answerer=self.callback_answerer,
109
+ role_list_presenter=RoleListPresenter(
110
+ message_sender=self.message_sender,
111
+ phrase_repo=self.phrase_repo,
112
+ role_repo=self.role_repo,
113
+ role_selection_handler_prefix=role_selection_handler.prefix,
114
+ ),
115
+ )
116
+ return self._show_roles_handler
117
+
118
+ def create_router(self) -> IRequestRoleFlowRouter:
119
+ role_selection_handler = self._get_role_selection_handler()
120
+
121
+ role_list_presenter = RoleListPresenter(
122
+ message_sender=self.message_sender,
123
+ phrase_repo=self.phrase_repo,
124
+ role_repo=self.role_repo,
125
+ role_selection_handler_prefix=role_selection_handler.prefix,
126
+ )
127
+
128
+ return RequestRoleFlowRouter(role_list_presenter=role_list_presenter)
129
+
130
+ def register_handlers(
131
+ self,
132
+ callback_registry: ICallbackHandlerRegistry,
133
+ message_registry: IMessageHandlerRegistry,
134
+ ) -> None:
135
+ callback_registry.register(self._get_show_roles_handler())
136
+ callback_registry.register(self._get_role_selection_handler())
137
+ callback_registry.register(self._get_approve_handler())
138
+ callback_registry.register(self._get_reject_handler())
139
+
140
+ router = self.create_router()
141
+ command_handler = RequestRoleCommandHandler(request_role_flow_router=router)
142
+ message_registry.register(
143
+ handler=command_handler,
144
+ commands=["request_role"],
145
+ content_types=["text"],
146
+ )
@@ -0,0 +1,23 @@
1
+ from bot_framework.flows.request_role_flow.handlers.approve_role_handler import (
2
+ ApproveRoleHandler,
3
+ )
4
+ from bot_framework.flows.request_role_flow.handlers.reject_role_handler import (
5
+ RejectRoleHandler,
6
+ )
7
+ from bot_framework.flows.request_role_flow.handlers.request_role_command_handler import (
8
+ RequestRoleCommandHandler,
9
+ )
10
+ from bot_framework.flows.request_role_flow.handlers.role_selection_handler import (
11
+ RoleSelectionHandler,
12
+ )
13
+ from bot_framework.flows.request_role_flow.handlers.show_roles_handler import (
14
+ ShowRolesHandler,
15
+ )
16
+
17
+ __all__ = [
18
+ "ApproveRoleHandler",
19
+ "RejectRoleHandler",
20
+ "RequestRoleCommandHandler",
21
+ "RoleSelectionHandler",
22
+ "ShowRolesHandler",
23
+ ]
@@ -0,0 +1,48 @@
1
+ from uuid import uuid4
2
+
3
+ from bot_framework.entities.bot_callback import BotCallback
4
+ from bot_framework.flows.request_role_flow.protocols import IRoleAssigner
5
+ from bot_framework.protocols.i_callback_answerer import ICallbackAnswerer
6
+ from bot_framework.role_management.repos.protocols.i_role_repo import IRoleRepo
7
+ from bot_framework.role_management.repos.protocols.i_user_repo import IUserRepo
8
+
9
+
10
+ class ApproveRoleHandler:
11
+ def __init__(
12
+ self,
13
+ callback_answerer: ICallbackAnswerer,
14
+ role_repo: IRoleRepo,
15
+ user_repo: IUserRepo,
16
+ role_assigner: IRoleAssigner,
17
+ ) -> None:
18
+ self.callback_answerer = callback_answerer
19
+ self.role_repo = role_repo
20
+ self.user_repo = user_repo
21
+ self.role_assigner = role_assigner
22
+ self.prefix = uuid4().hex
23
+ self.allowed_roles: set[str] | None = None
24
+
25
+ def handle(self, callback: BotCallback) -> None:
26
+ self.callback_answerer.answer(callback_query_id=callback.id)
27
+
28
+ if not callback.data:
29
+ raise ValueError("callback.data is required but was None")
30
+
31
+ parts = callback.data.split(":")
32
+ if len(parts) < 3:
33
+ raise ValueError(f"Invalid callback data format: {callback.data}")
34
+
35
+ role_id = int(parts[1])
36
+ user_id = int(parts[2])
37
+
38
+ user = self.user_repo.find_by_id(id=user_id)
39
+ if not user:
40
+ return
41
+
42
+ language_code = user.language_code
43
+
44
+ self.role_assigner.assign_and_notify(
45
+ user_id=user_id,
46
+ role_id=role_id,
47
+ language_code=language_code,
48
+ )
@@ -0,0 +1,46 @@
1
+ from uuid import uuid4
2
+
3
+ from bot_framework.entities.bot_callback import BotCallback
4
+ from bot_framework.flows.request_role_flow.protocols import IRoleRejectionNotifier
5
+ from bot_framework.protocols.i_callback_answerer import ICallbackAnswerer
6
+ from bot_framework.role_management.repos.protocols.i_role_repo import IRoleRepo
7
+ from bot_framework.role_management.repos.protocols.i_user_repo import IUserRepo
8
+
9
+
10
+ class RejectRoleHandler:
11
+ def __init__(
12
+ self,
13
+ callback_answerer: ICallbackAnswerer,
14
+ role_repo: IRoleRepo,
15
+ user_repo: IUserRepo,
16
+ role_rejection_notifier: IRoleRejectionNotifier,
17
+ ) -> None:
18
+ self.callback_answerer = callback_answerer
19
+ self.role_repo = role_repo
20
+ self.user_repo = user_repo
21
+ self.role_rejection_notifier = role_rejection_notifier
22
+ self.prefix = uuid4().hex
23
+ self.allowed_roles: set[str] | None = None
24
+
25
+ def handle(self, callback: BotCallback) -> None:
26
+ self.callback_answerer.answer(callback_query_id=callback.id)
27
+
28
+ if not callback.data:
29
+ raise ValueError("callback.data is required but was None")
30
+
31
+ parts = callback.data.split(":")
32
+ if len(parts) < 3:
33
+ raise ValueError(f"Invalid callback data format: {callback.data}")
34
+
35
+ user_id = int(parts[2])
36
+
37
+ user = self.user_repo.find_by_id(id=user_id)
38
+ if not user:
39
+ return
40
+
41
+ language_code = user.language_code
42
+
43
+ self.role_rejection_notifier.notify(
44
+ user_id=user_id,
45
+ language_code=language_code,
46
+ )
@@ -0,0 +1,24 @@
1
+ from bot_framework.entities.bot_message import BotMessage
2
+ from bot_framework.flows.request_role_flow.protocols import (
3
+ IRequestRoleFlowRouter,
4
+ )
5
+ from bot_framework.protocols.i_message_handler import IMessageHandler
6
+
7
+
8
+ class RequestRoleCommandHandler(IMessageHandler):
9
+ allowed_roles: set[str] | None = None
10
+
11
+ def __init__(
12
+ self,
13
+ request_role_flow_router: IRequestRoleFlowRouter,
14
+ ) -> None:
15
+ self.request_role_flow_router = request_role_flow_router
16
+
17
+ def handle(self, message: BotMessage) -> None:
18
+ if not message.from_user:
19
+ raise ValueError("message.from_user is required but was None")
20
+
21
+ self.request_role_flow_router.start(
22
+ user=message.from_user,
23
+ chat_id=message.chat_id,
24
+ )