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,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,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,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,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,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,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
|
+
]
|