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,16 @@
1
+ from typing import (
2
+ Protocol,
3
+ runtime_checkable,
4
+ )
5
+
6
+ from bot_framework.entities.bot_message import BotMessage
7
+
8
+
9
+ @runtime_checkable
10
+ class IMessageHandler(Protocol):
11
+ allowed_roles: set[str] | None
12
+
13
+ def handle(
14
+ self,
15
+ message: BotMessage,
16
+ ) -> bool | None: ...
@@ -0,0 +1,14 @@
1
+ from collections.abc import Callable
2
+ from typing import Protocol
3
+
4
+ from bot_framework.protocols.i_message_handler import IMessageHandler
5
+
6
+
7
+ class IMessageHandlerRegistry(Protocol):
8
+ def register(
9
+ self,
10
+ handler: IMessageHandler,
11
+ commands: list[str] | None = None,
12
+ content_types: list[str] | None = None,
13
+ func: Callable[..., bool] | None = None,
14
+ ) -> None: ...
@@ -0,0 +1,17 @@
1
+ from typing import Protocol
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
+
7
+
8
+ class IMessageReplacer(Protocol):
9
+ def replace(
10
+ self,
11
+ chat_id: int,
12
+ message_id: int,
13
+ text: str,
14
+ parse_mode: ParseMode = ParseMode.HTML,
15
+ keyboard: Keyboard | None = None,
16
+ flow_name: str | None = None,
17
+ ) -> BotMessage: ...
@@ -0,0 +1,31 @@
1
+ from typing import Protocol
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
+
7
+
8
+ class IMessageSender(Protocol):
9
+ def send(
10
+ self,
11
+ chat_id: int,
12
+ text: str,
13
+ parse_mode: ParseMode = ParseMode.HTML,
14
+ keyboard: Keyboard | None = None,
15
+ flow_name: str | None = None,
16
+ ) -> BotMessage: ...
17
+
18
+ def send_markdown_as_html(
19
+ self,
20
+ chat_id: int,
21
+ text: str,
22
+ keyboard: Keyboard | None = None,
23
+ flow_name: str | None = None,
24
+ ) -> BotMessage: ...
25
+
26
+ def send_document(
27
+ self,
28
+ chat_id: int,
29
+ document: bytes,
30
+ filename: str,
31
+ ) -> BotMessage: ...
@@ -0,0 +1,16 @@
1
+ from typing import Protocol
2
+
3
+ from bot_framework.protocols.i_message_deleter import IMessageDeleter
4
+ from bot_framework.protocols.i_message_replacer import IMessageReplacer
5
+ from bot_framework.protocols.i_message_sender import IMessageSender
6
+ from bot_framework.protocols.i_notify_replacer import INotifyReplacer
7
+
8
+
9
+ class IMessageService(
10
+ IMessageSender,
11
+ IMessageReplacer,
12
+ IMessageDeleter,
13
+ INotifyReplacer,
14
+ Protocol,
15
+ ):
16
+ pass
@@ -0,0 +1,12 @@
1
+ from typing import Protocol
2
+
3
+ from bot_framework.entities.bot_message import BotMessage
4
+ from bot_framework.protocols.i_message_handler import IMessageHandler
5
+
6
+
7
+ class INextStepHandlerRegistrar(Protocol):
8
+ def register(
9
+ self,
10
+ message: BotMessage,
11
+ handler: IMessageHandler,
12
+ ) -> None: ...
@@ -0,0 +1,17 @@
1
+ from typing import Protocol
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
+
7
+
8
+ class INotifyReplacer(Protocol):
9
+ def notify_replace(
10
+ self,
11
+ chat_id: int,
12
+ message_id: int,
13
+ text: str,
14
+ parse_mode: ParseMode = ParseMode.HTML,
15
+ keyboard: Keyboard | None = None,
16
+ flow_name: str | None = None,
17
+ ) -> BotMessage: ...
@@ -0,0 +1,6 @@
1
+ from datetime import datetime
2
+ from typing import Protocol
3
+
4
+
5
+ class IRemainingTimeFormatter(Protocol):
6
+ def format(self, expires_at: datetime, language_code: str) -> str: ...
@@ -0,0 +1,13 @@
1
+ from bot_framework.role_management.entities import Role, RoleName, User, UserRole
2
+ from bot_framework.role_management.repos import RoleRepo
3
+ from bot_framework.role_management.repos.protocols import IRoleRepo, IUserRepo
4
+
5
+ __all__ = [
6
+ "User",
7
+ "Role",
8
+ "UserRole",
9
+ "RoleName",
10
+ "RoleRepo",
11
+ "IRoleRepo",
12
+ "IUserRepo",
13
+ ]
@@ -0,0 +1,10 @@
1
+ from bot_framework.entities import Role, RoleName, User
2
+
3
+ from .user_role import UserRole
4
+
5
+ __all__ = [
6
+ "User",
7
+ "Role",
8
+ "UserRole",
9
+ "RoleName",
10
+ ]
@@ -0,0 +1,13 @@
1
+ from datetime import datetime
2
+
3
+ from pydantic import (
4
+ BaseModel, ConfigDict, Field,
5
+ )
6
+
7
+
8
+ class UserRole(BaseModel):
9
+ user_id: int
10
+ role_id: int
11
+ assigned_at: datetime = Field(default_factory=datetime.now)
12
+
13
+ model_config = ConfigDict(from_attributes=True)
@@ -0,0 +1,5 @@
1
+ from .role_repo import RoleRepo
2
+
3
+ __all__ = [
4
+ "RoleRepo",
5
+ ]
@@ -0,0 +1,7 @@
1
+ from .i_role_repo import IRoleRepo
2
+ from .i_user_repo import IUserRepo
3
+
4
+ __all__ = [
5
+ "IUserRepo",
6
+ "IRoleRepo",
7
+ ]
@@ -0,0 +1,30 @@
1
+ from typing import Protocol
2
+
3
+ from bot_framework.base_protocols import (
4
+ GetAllProtocol,
5
+ ReadProtocol,
6
+ )
7
+ from bot_framework.entities.role import Role
8
+
9
+
10
+ class IRoleRepo(
11
+ GetAllProtocol,
12
+ ReadProtocol,
13
+ Protocol,
14
+ ):
15
+ def get_user_roles(
16
+ self,
17
+ user_id: int,
18
+ ) -> list[Role]: ...
19
+
20
+ def assign_role(
21
+ self,
22
+ user_id: int,
23
+ role_id: int,
24
+ ) -> None: ...
25
+
26
+ def assign_role_by_name(
27
+ self,
28
+ user_id: int,
29
+ role_name: str,
30
+ ) -> None: ...
@@ -0,0 +1,24 @@
1
+ from typing import Protocol
2
+
3
+ from bot_framework.base_protocols import (
4
+ CreateProtocol,
5
+ DeleteProtocol,
6
+ GetByNameProtocol,
7
+ ReadProtocol,
8
+ UpdateProtocol,
9
+ )
10
+ from bot_framework.entities.user import User
11
+
12
+
13
+ class IUserRepo(
14
+ GetByNameProtocol,
15
+ CreateProtocol,
16
+ DeleteProtocol,
17
+ ReadProtocol,
18
+ UpdateProtocol,
19
+ Protocol,
20
+ ):
21
+ def get_by_role_name(
22
+ self,
23
+ role_name: str,
24
+ ) -> list[User]: ...
@@ -0,0 +1,103 @@
1
+ import psycopg
2
+ from psycopg.rows import class_row
3
+
4
+ from bot_framework.entities.role import Role
5
+ from bot_framework.role_management.repos.protocols.i_role_repo import IRoleRepo
6
+
7
+
8
+ class RoleRepo(IRoleRepo):
9
+ def __init__(
10
+ self,
11
+ database_url: str,
12
+ ):
13
+ self.database_url = database_url
14
+
15
+ def get_all(self) -> list[Role]:
16
+ with psycopg.connect(self.database_url) as conn:
17
+ with conn.cursor(row_factory=class_row(Role)) as cur:
18
+ cur.execute(
19
+ "SELECT * FROM roles WHERE is_active = TRUE ORDER BY name",
20
+ )
21
+ return cur.fetchall()
22
+
23
+ def find_by_id(
24
+ self,
25
+ id: int,
26
+ ) -> Role | None:
27
+ with psycopg.connect(self.database_url) as conn:
28
+ with conn.cursor(row_factory=class_row(Role)) as cur:
29
+ cur.execute(
30
+ "SELECT * FROM roles WHERE id = %(role_id)s AND is_active = TRUE",
31
+ {
32
+ "role_id": id,
33
+ },
34
+ )
35
+ return cur.fetchone()
36
+
37
+ def get_by_id(
38
+ self,
39
+ id: int,
40
+ ) -> Role:
41
+ role = self.find_by_id(id)
42
+ if not role:
43
+ raise ValueError(f"Role with id {id} not found")
44
+ return role
45
+
46
+ def get_user_roles(
47
+ self,
48
+ user_id: int,
49
+ ) -> list[Role]:
50
+ with psycopg.connect(self.database_url) as conn:
51
+ with conn.cursor(row_factory=class_row(Role)) as cur:
52
+ cur.execute(
53
+ """
54
+ SELECT r.*
55
+ FROM user_roles ur
56
+ JOIN roles r ON ur.role_id = r.id
57
+ WHERE ur.user_id = %(user_id)s AND r.is_active = TRUE
58
+ """,
59
+ {
60
+ "user_id": user_id,
61
+ },
62
+ )
63
+ return cur.fetchall()
64
+
65
+ def assign_role(
66
+ self,
67
+ user_id: int,
68
+ role_id: int,
69
+ ) -> None:
70
+ with psycopg.connect(self.database_url) as conn:
71
+ with conn.cursor() as cur:
72
+ cur.execute(
73
+ """
74
+ INSERT INTO user_roles (user_id, role_id)
75
+ VALUES (%(user_id)s, %(role_id)s)
76
+ ON CONFLICT (user_id, role_id) DO NOTHING
77
+ """,
78
+ {
79
+ "user_id": user_id,
80
+ "role_id": role_id,
81
+ },
82
+ )
83
+
84
+ def assign_role_by_name(
85
+ self,
86
+ user_id: int,
87
+ role_name: str,
88
+ ) -> None:
89
+ with psycopg.connect(self.database_url) as conn:
90
+ with conn.cursor() as cur:
91
+ cur.execute(
92
+ """
93
+ INSERT INTO user_roles (user_id, role_id)
94
+ SELECT %(user_id)s, r.id
95
+ FROM roles r
96
+ WHERE r.name = %(role_name)s AND r.is_active = TRUE
97
+ ON CONFLICT (user_id, role_id) DO NOTHING
98
+ """,
99
+ {
100
+ "user_id": user_id,
101
+ "role_name": role_name,
102
+ },
103
+ )
@@ -0,0 +1 @@
1
+ __all__ = []
@@ -0,0 +1 @@
1
+ __all__ = []
@@ -0,0 +1,9 @@
1
+ from .card_field_formatter import CardFieldFormatter
2
+ from .display_width_calculator import DisplayWidthCalculator
3
+ from .remaining_time_formatter import RemainingTimeFormatter
4
+
5
+ __all__ = [
6
+ "CardFieldFormatter",
7
+ "DisplayWidthCalculator",
8
+ "RemainingTimeFormatter",
9
+ ]
@@ -0,0 +1,43 @@
1
+ from bot_framework.protocols import ICardFieldFormatter, IDisplayWidthCalculator
2
+ from bot_framework.services.display_width_calculator import DisplayWidthCalculator
3
+
4
+
5
+ class CardFieldFormatter(ICardFieldFormatter):
6
+ def __init__(
7
+ self,
8
+ line_length: int = 43,
9
+ display_width_calculator: IDisplayWidthCalculator | None = None,
10
+ ):
11
+ self.line_length = line_length
12
+ self._display_width_calculator = display_width_calculator or DisplayWidthCalculator()
13
+
14
+ def display_width(self, text: str) -> int:
15
+ return self._display_width_calculator.calculate(text)
16
+
17
+ def generate_field_lines(self, label: str, value: str) -> list[str]:
18
+ label_width = self.display_width(label)
19
+ value_width = self.display_width(value)
20
+
21
+ if value_width + label_width + 3 <= self.line_length:
22
+ dots_count = self.line_length - 2 - label_width - value_width
23
+ dots = "." * max(1, dots_count)
24
+ return [f"{label} {dots} {value}"]
25
+
26
+ lines = [f"{label}:"]
27
+ if value_width <= self.line_length:
28
+ lines.append(value)
29
+ else:
30
+ words = value.split()
31
+ current_line = ""
32
+ for word in words:
33
+ if not current_line:
34
+ current_line = word
35
+ elif self.display_width(current_line) + self.display_width(word) + 1 <= self.line_length:
36
+ current_line += " " + word
37
+ else:
38
+ lines.append(current_line)
39
+ current_line = word
40
+ if current_line:
41
+ lines.append(current_line)
42
+
43
+ return lines
@@ -0,0 +1,45 @@
1
+ import unicodedata
2
+
3
+ from bot_framework.protocols import IDisplayWidthCalculator
4
+
5
+
6
+ class DisplayWidthCalculator(IDisplayWidthCalculator):
7
+ def calculate(self, text: str) -> int:
8
+ width = 0
9
+ i = 0
10
+ chars = list(text)
11
+ while i < len(chars):
12
+ char = chars[i]
13
+ code_point = ord(char)
14
+
15
+ if 0x1F1E6 <= code_point <= 0x1F1FF:
16
+ if i + 1 < len(chars):
17
+ next_code = ord(chars[i + 1])
18
+ if 0x1F1E6 <= next_code <= 0x1F1FF:
19
+ width += 3
20
+ i += 2
21
+ continue
22
+ width += 1
23
+ i += 1
24
+ continue
25
+
26
+ if self._is_emoji(code_point):
27
+ width += 3
28
+ i += 1
29
+ continue
30
+
31
+ ea_width = unicodedata.east_asian_width(char)
32
+ if ea_width in ("W", "F"):
33
+ width += 2
34
+ else:
35
+ width += 1
36
+ i += 1
37
+
38
+ return width
39
+
40
+ def _is_emoji(self, code_point: int) -> bool:
41
+ return (
42
+ 0x1F300 <= code_point <= 0x1F9FF
43
+ or 0x2600 <= code_point <= 0x26FF
44
+ or 0x2700 <= code_point <= 0x27BF
45
+ )
@@ -0,0 +1,31 @@
1
+ from datetime import UTC, datetime
2
+
3
+ from bot_framework.language_management.repos.protocols.i_phrase_repo import IPhraseRepo
4
+ from bot_framework.protocols import IRemainingTimeFormatter
5
+
6
+
7
+ class RemainingTimeFormatter(IRemainingTimeFormatter):
8
+ def __init__(self, phrase_repo: IPhraseRepo) -> None:
9
+ self._phrase_repo = phrase_repo
10
+
11
+ def format(self, expires_at: datetime, language_code: str) -> str:
12
+ if expires_at.tzinfo is None:
13
+ expires_at = expires_at.replace(tzinfo=UTC)
14
+
15
+ remaining = expires_at - datetime.now(UTC)
16
+ total_minutes = max(0, int(remaining.total_seconds() // 60))
17
+ hours = total_minutes // 60
18
+ minutes = total_minutes % 60
19
+
20
+ hours_suffix = self._phrase_repo.get_phrase(
21
+ key="shared.time.hours_short",
22
+ language_code=language_code,
23
+ )
24
+ minutes_suffix = self._phrase_repo.get_phrase(
25
+ key="shared.time.minutes_short",
26
+ language_code=language_code,
27
+ )
28
+
29
+ if hours > 0:
30
+ return f"{hours}{hours_suffix} {minutes}{minutes_suffix}"
31
+ return f"{minutes}{minutes_suffix}"
@@ -0,0 +1,56 @@
1
+ from .middleware import EnsureUserMiddleware, IMiddleware
2
+ from .protocols import (
3
+ ICallbackHandler,
4
+ ICallbackHandlerRegistry,
5
+ IEnsureUserExists,
6
+ IFlowRouter,
7
+ IMarkdownEscaper,
8
+ IMarkdownToHtmlConverter,
9
+ IMessageHandler,
10
+ IMessageHandlerRegistry,
11
+ IMessageSender,
12
+ INextStepHandlerRegistrar,
13
+ )
14
+ from .services import (
15
+ CallbackAnswerer,
16
+ CallbackHandlerRegistry,
17
+ CloseCallbackHandler,
18
+ EnsureUserExists,
19
+ MarkdownEscaper,
20
+ MarkdownToHtmlConverter,
21
+ MessageHandlerRegistry,
22
+ NextStepHandlerRegistrar,
23
+ TelegramMessageCore,
24
+ TelegramMessageDeleter,
25
+ TelegramMessageReplacer,
26
+ TelegramMessageSender,
27
+ TelegramNotifyReplacer,
28
+ )
29
+
30
+ __all__ = [
31
+ "CallbackAnswerer",
32
+ "CallbackHandlerRegistry",
33
+ "CloseCallbackHandler",
34
+ "EnsureUserExists",
35
+ "EnsureUserMiddleware",
36
+ "ICallbackHandler",
37
+ "ICallbackHandlerRegistry",
38
+ "IEnsureUserExists",
39
+ "IFlowRouter",
40
+ "IMarkdownEscaper",
41
+ "IMarkdownToHtmlConverter",
42
+ "IMessageHandler",
43
+ "IMessageHandlerRegistry",
44
+ "IMessageSender",
45
+ "IMiddleware",
46
+ "INextStepHandlerRegistrar",
47
+ "MarkdownEscaper",
48
+ "MarkdownToHtmlConverter",
49
+ "MessageHandlerRegistry",
50
+ "NextStepHandlerRegistrar",
51
+ "TelegramMessageCore",
52
+ "TelegramMessageDeleter",
53
+ "TelegramMessageReplacer",
54
+ "TelegramMessageSender",
55
+ "TelegramNotifyReplacer",
56
+ ]
@@ -0,0 +1,4 @@
1
+ from .ensure_user_middleware import EnsureUserMiddleware
2
+ from .i_middleware import IMiddleware
3
+
4
+ __all__ = ["EnsureUserMiddleware", "IMiddleware"]
@@ -0,0 +1,46 @@
1
+ from telebot.types import CallbackQuery, Message, User as TelegramUser
2
+
3
+ from bot_framework.entities.bot_user import BotUser
4
+ from bot_framework.protocols import IEnsureUserExists
5
+ from bot_framework.telegram.middleware.i_middleware import IMiddleware
6
+
7
+
8
+ class EnsureUserMiddleware(IMiddleware):
9
+ update_types = ["message", "callback_query"]
10
+
11
+ def __init__(self, ensure_user_exists: IEnsureUserExists):
12
+ super().__init__()
13
+ self.ensure_user_exists = ensure_user_exists
14
+ self.update_sensitive = False
15
+
16
+ def pre_process(
17
+ self,
18
+ message: Message | CallbackQuery,
19
+ data: dict[str, object],
20
+ ) -> None:
21
+ telegram_user = message.from_user
22
+ if not telegram_user:
23
+ return
24
+ bot_user = self._to_bot_user(telegram_user)
25
+ self.ensure_user_exists.execute(user=bot_user)
26
+
27
+ def _to_bot_user(self, telegram_user: TelegramUser) -> BotUser:
28
+ bot_user = BotUser(
29
+ id=telegram_user.id,
30
+ username=telegram_user.username,
31
+ first_name=telegram_user.first_name,
32
+ last_name=telegram_user.last_name,
33
+ language_code=telegram_user.language_code or "en",
34
+ is_bot=telegram_user.is_bot,
35
+ is_premium=telegram_user.is_premium or False,
36
+ )
37
+ bot_user.set_original(telegram_user)
38
+ return bot_user
39
+
40
+ def post_process(
41
+ self,
42
+ message: Message | CallbackQuery,
43
+ data: dict[str, object],
44
+ exception: Exception | None,
45
+ ) -> None:
46
+ pass
@@ -0,0 +1,7 @@
1
+ from telebot import TeleBot
2
+ from telebot.handler_backends import BaseMiddleware
3
+
4
+
5
+ class IMiddleware(BaseMiddleware):
6
+ def register(self, bot: TeleBot) -> None:
7
+ bot.setup_middleware(self)
@@ -0,0 +1,36 @@
1
+ from bot_framework.protocols import (
2
+ ICallbackAnswerer,
3
+ ICallbackHandler,
4
+ ICallbackHandlerRegistry,
5
+ IEnsureUserExists,
6
+ IFlowRouter,
7
+ IMarkdownToHtmlConverter,
8
+ IMessageDeleter,
9
+ IMessageHandler,
10
+ IMessageHandlerRegistry,
11
+ IMessageReplacer,
12
+ IMessageSender,
13
+ IMessageService,
14
+ INextStepHandlerRegistrar,
15
+ INotifyReplacer,
16
+ )
17
+
18
+ from .i_markdown_escaper import IMarkdownEscaper
19
+
20
+ __all__ = [
21
+ "ICallbackAnswerer",
22
+ "ICallbackHandler",
23
+ "ICallbackHandlerRegistry",
24
+ "IEnsureUserExists",
25
+ "IFlowRouter",
26
+ "IMarkdownEscaper",
27
+ "IMarkdownToHtmlConverter",
28
+ "IMessageDeleter",
29
+ "IMessageHandler",
30
+ "IMessageHandlerRegistry",
31
+ "IMessageReplacer",
32
+ "IMessageSender",
33
+ "IMessageService",
34
+ "INextStepHandlerRegistrar",
35
+ "INotifyReplacer",
36
+ ]
@@ -0,0 +1,5 @@
1
+ from typing import Protocol
2
+
3
+
4
+ class IMarkdownEscaper(Protocol):
5
+ def escape(self, text: str) -> str: ...