aiogram-cmds 0.1.0__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.
- aiogram_cmds/__init__.py +69 -0
- aiogram_cmds/apply.py +88 -0
- aiogram_cmds/auto_setup.py +161 -0
- aiogram_cmds/builder.py +77 -0
- aiogram_cmds/builders_configured.py +69 -0
- aiogram_cmds/customize.py +67 -0
- aiogram_cmds/dynamic.py +39 -0
- aiogram_cmds/manager.py +148 -0
- aiogram_cmds/policy.py +38 -0
- aiogram_cmds/registry.py +21 -0
- aiogram_cmds/setters.py +92 -0
- aiogram_cmds/settings.py +54 -0
- aiogram_cmds/translator.py +33 -0
- aiogram_cmds/version.py +3 -0
- aiogram_cmds-0.1.0.dist-info/METADATA +282 -0
- aiogram_cmds-0.1.0.dist-info/RECORD +18 -0
- aiogram_cmds-0.1.0.dist-info/WHEEL +4 -0
- aiogram_cmds-0.1.0.dist-info/licenses/LICENSE +21 -0
aiogram_cmds/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
aiogram_cmds - Command builder & scope manager for aiogram bots.
|
|
3
|
+
|
|
4
|
+
This package provides both simple and full customizable modes for managing
|
|
5
|
+
Telegram bot commands with i18n support, profiles, and multiple scope types.
|
|
6
|
+
|
|
7
|
+
Simple Mode:
|
|
8
|
+
from aiogram_cmds import CommandScopeManager, load_settings, build_translator_from_i18n
|
|
9
|
+
|
|
10
|
+
settings = load_settings()
|
|
11
|
+
translator = build_translator_from_i18n(i18n)
|
|
12
|
+
manager = CommandScopeManager(bot, settings=settings, translator=translator)
|
|
13
|
+
await manager.setup_all()
|
|
14
|
+
|
|
15
|
+
Full Customizable Mode:
|
|
16
|
+
from aiogram_cmds import CmdsConfig, CommandDef, ProfileDef, ScopeDef, MenuButtonDef
|
|
17
|
+
|
|
18
|
+
config = CmdsConfig(
|
|
19
|
+
commands={"start": CommandDef(i18n_key="start.desc")},
|
|
20
|
+
profiles={"guest": ProfileDef(include=["start"])},
|
|
21
|
+
scopes=[ScopeDef(scope="all_private_chats", profile="guest")],
|
|
22
|
+
)
|
|
23
|
+
manager = CommandScopeManager(bot, config=config, profile_resolver=my_resolver)
|
|
24
|
+
|
|
25
|
+
Auto-Setup (Recommended):
|
|
26
|
+
from aiogram_cmds.auto_setup import setup_commands_auto
|
|
27
|
+
|
|
28
|
+
async def on_startup():
|
|
29
|
+
command_manager = await setup_commands_auto(bot)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from .apply import apply_config
|
|
33
|
+
from .auto_setup import setup_commands_auto
|
|
34
|
+
from .builder import build_bot_commands
|
|
35
|
+
from .customize import CmdsConfig, CommandDef, MenuButtonDef, ProfileDef, ScopeDef
|
|
36
|
+
from .dynamic import set_user_profile
|
|
37
|
+
from .manager import CommandScopeManager
|
|
38
|
+
from .policy import CommandPolicy, Flags, ProfileResolver, default_policy
|
|
39
|
+
from .settings import CmdsSettings, load_settings
|
|
40
|
+
from .translator import Translator, build_translator_from_i18n, noop_translator
|
|
41
|
+
|
|
42
|
+
__version__ = "0.1.0"
|
|
43
|
+
__author__ = "aiogram_cmds contributors"
|
|
44
|
+
__description__ = "Command builder & scope manager for aiogram bots"
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
# Simple / classic
|
|
48
|
+
"build_bot_commands",
|
|
49
|
+
"CommandScopeManager",
|
|
50
|
+
"Flags",
|
|
51
|
+
"CommandPolicy",
|
|
52
|
+
"default_policy",
|
|
53
|
+
"Translator",
|
|
54
|
+
"build_translator_from_i18n",
|
|
55
|
+
"noop_translator",
|
|
56
|
+
"CmdsSettings",
|
|
57
|
+
"load_settings",
|
|
58
|
+
# Full customizable
|
|
59
|
+
"CmdsConfig",
|
|
60
|
+
"CommandDef",
|
|
61
|
+
"ProfileDef",
|
|
62
|
+
"ScopeDef",
|
|
63
|
+
"MenuButtonDef",
|
|
64
|
+
"apply_config",
|
|
65
|
+
"set_user_profile",
|
|
66
|
+
"ProfileResolver",
|
|
67
|
+
# Auto-setup (convenience)
|
|
68
|
+
"setup_commands_auto",
|
|
69
|
+
]
|
aiogram_cmds/apply.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Apply full configurations to Telegram bot.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
|
|
7
|
+
from aiogram import Bot
|
|
8
|
+
from aiogram.types import (
|
|
9
|
+
BotCommandScopeAllChatAdministrators,
|
|
10
|
+
BotCommandScopeAllGroupChats,
|
|
11
|
+
BotCommandScopeAllPrivateChats,
|
|
12
|
+
BotCommandScopeChat,
|
|
13
|
+
BotCommandScopeChatAdministrators,
|
|
14
|
+
BotCommandScopeChatMember,
|
|
15
|
+
BotCommandScopeDefault,
|
|
16
|
+
MenuButtonCommands,
|
|
17
|
+
MenuButtonDefault,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from .builders_configured import build_from_config
|
|
21
|
+
from .customize import CmdsConfig, ScopeDef
|
|
22
|
+
from .registry import CommandRegistry
|
|
23
|
+
from .translator import Translator
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _scope_obj(s: ScopeDef):
|
|
27
|
+
if s.scope == "default":
|
|
28
|
+
return BotCommandScopeDefault()
|
|
29
|
+
if s.scope == "all_private_chats":
|
|
30
|
+
return BotCommandScopeAllPrivateChats()
|
|
31
|
+
if s.scope == "all_group_chats":
|
|
32
|
+
return BotCommandScopeAllGroupChats()
|
|
33
|
+
if s.scope == "all_chat_admins":
|
|
34
|
+
return BotCommandScopeAllChatAdministrators()
|
|
35
|
+
if s.scope == "chat":
|
|
36
|
+
return BotCommandScopeChat(chat_id=s.chat_id)
|
|
37
|
+
if s.scope == "chat_admins":
|
|
38
|
+
return BotCommandScopeChatAdministrators(chat_id=s.chat_id)
|
|
39
|
+
if s.scope == "chat_member":
|
|
40
|
+
return BotCommandScopeChatMember(chat_id=s.chat_id, user_id=s.user_id)
|
|
41
|
+
raise ValueError(f"Unknown scope: {s.scope}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _sorted_scopes(scopes: Iterable[ScopeDef]) -> list[ScopeDef]:
|
|
45
|
+
rank = {
|
|
46
|
+
"chat_member": 0,
|
|
47
|
+
"chat_admins": 1,
|
|
48
|
+
"chat": 2,
|
|
49
|
+
"all_chat_admins": 3,
|
|
50
|
+
"all_group_chats": 4,
|
|
51
|
+
"all_private_chats": 5,
|
|
52
|
+
"default": 6,
|
|
53
|
+
}
|
|
54
|
+
return sorted(scopes, key=lambda s: rank[s.scope])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def apply_config(
|
|
58
|
+
bot: Bot,
|
|
59
|
+
cfg: CmdsConfig,
|
|
60
|
+
*,
|
|
61
|
+
translator: Translator | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Apply all configured scopes + menu button in order of specificity.
|
|
65
|
+
"""
|
|
66
|
+
reg = CommandRegistry(cfg)
|
|
67
|
+
scopes = _sorted_scopes(cfg.scopes)
|
|
68
|
+
langs = cfg.languages or ["en"]
|
|
69
|
+
|
|
70
|
+
for s in scopes:
|
|
71
|
+
scope_obj = _scope_obj(s)
|
|
72
|
+
target_langs = s.languages or langs
|
|
73
|
+
names = list(reg.resolve_profile_commands(s.profile))
|
|
74
|
+
for lang in target_langs:
|
|
75
|
+
cmds = build_from_config(
|
|
76
|
+
reg,
|
|
77
|
+
names,
|
|
78
|
+
lang=lang,
|
|
79
|
+
translator=translator,
|
|
80
|
+
key_prefix=cfg.i18n_key_prefix,
|
|
81
|
+
)
|
|
82
|
+
await bot.set_my_commands(cmds, scope=scope_obj, language_code=lang)
|
|
83
|
+
|
|
84
|
+
# menu button (global default)
|
|
85
|
+
if cfg.menu_button.mode == "commands":
|
|
86
|
+
await bot.set_chat_menu_button(menu_button=MenuButtonCommands())
|
|
87
|
+
else:
|
|
88
|
+
await bot.set_chat_menu_button(menu_button=MenuButtonDefault())
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-setup utilities for aiogram_cmds.
|
|
3
|
+
|
|
4
|
+
This module provides convenient functions to automatically set up command management
|
|
5
|
+
with minimal configuration, making it easy to integrate into __main__.py.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from aiogram import Bot
|
|
11
|
+
|
|
12
|
+
from .customize import CmdsConfig, CommandDef, MenuButtonDef, ProfileDef, ScopeDef
|
|
13
|
+
from .manager import CommandScopeManager
|
|
14
|
+
from .policy import Flags, ProfileResolver
|
|
15
|
+
from .translator import build_translator_from_i18n
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def setup_commands_auto(
|
|
21
|
+
bot: Bot,
|
|
22
|
+
*,
|
|
23
|
+
languages: list[str] | None = None,
|
|
24
|
+
i18n_instance=None,
|
|
25
|
+
config: CmdsConfig | None = None,
|
|
26
|
+
profile_resolver: ProfileResolver | None = None,
|
|
27
|
+
) -> CommandScopeManager:
|
|
28
|
+
"""
|
|
29
|
+
Automatically set up command management with minimal configuration.
|
|
30
|
+
|
|
31
|
+
This function provides a simple way to set up commands in __main__.py
|
|
32
|
+
with sensible defaults and automatic i18n integration.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
bot: Telegram Bot instance
|
|
36
|
+
languages: List of supported languages (default: ["en"])
|
|
37
|
+
i18n_instance: aiogram i18n instance (optional, will try to import from common locations)
|
|
38
|
+
config: Custom CmdsConfig (optional, will create default if not provided)
|
|
39
|
+
profile_resolver: Custom ProfileResolver (optional, will create default if not provided)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
CommandScopeManager instance ready to use
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
# In __main__.py
|
|
46
|
+
from aiogram_cmds.auto_setup import setup_commands_auto
|
|
47
|
+
|
|
48
|
+
async def on_startup(dp, app_context=None):
|
|
49
|
+
command_manager = await setup_commands_auto(bot)
|
|
50
|
+
# Commands are automatically set up!
|
|
51
|
+
"""
|
|
52
|
+
# Default languages
|
|
53
|
+
if languages is None:
|
|
54
|
+
languages = ["en"]
|
|
55
|
+
|
|
56
|
+
# Try to get i18n instance if not provided
|
|
57
|
+
if i18n_instance is None:
|
|
58
|
+
i18n_instance = _try_import_i18n()
|
|
59
|
+
|
|
60
|
+
# Build translator
|
|
61
|
+
translator = build_translator_from_i18n(i18n_instance) if i18n_instance else None
|
|
62
|
+
|
|
63
|
+
# Create default config if not provided
|
|
64
|
+
if config is None:
|
|
65
|
+
config = create_default_config(languages)
|
|
66
|
+
|
|
67
|
+
# Create default profile resolver if not provided
|
|
68
|
+
if profile_resolver is None:
|
|
69
|
+
profile_resolver = create_default_profile_resolver()
|
|
70
|
+
|
|
71
|
+
# Create and setup command manager
|
|
72
|
+
manager = CommandScopeManager(
|
|
73
|
+
bot,
|
|
74
|
+
config=config,
|
|
75
|
+
translator=translator,
|
|
76
|
+
profile_resolver=profile_resolver,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Setup all command scopes and menu button
|
|
80
|
+
await manager.setup_all()
|
|
81
|
+
|
|
82
|
+
logger.info(
|
|
83
|
+
f"✅ Command management set up with {len(config.commands)} commands and {len(config.profiles)} profiles"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return manager
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _try_import_i18n():
|
|
90
|
+
"""Try to import i18n from common locations."""
|
|
91
|
+
# Common aiogram i18n import patterns
|
|
92
|
+
import_patterns = [
|
|
93
|
+
"bot.core.i18n.i18n",
|
|
94
|
+
"bot.i18n.i18n",
|
|
95
|
+
"i18n.i18n",
|
|
96
|
+
"bot.core.i18n",
|
|
97
|
+
"bot.i18n",
|
|
98
|
+
"i18n",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
for pattern in import_patterns:
|
|
102
|
+
try:
|
|
103
|
+
module_parts = pattern.split(".")
|
|
104
|
+
module = __import__(pattern, fromlist=[module_parts[-1]])
|
|
105
|
+
# Try to get the i18n instance
|
|
106
|
+
if hasattr(module, "i18n"):
|
|
107
|
+
return module.i18n
|
|
108
|
+
elif hasattr(module, "gettext"):
|
|
109
|
+
return module
|
|
110
|
+
except ImportError:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
logger.debug(
|
|
114
|
+
"Could not find i18n instance, commands will use fallback descriptions"
|
|
115
|
+
)
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def create_default_config(languages: list[str]) -> CmdsConfig:
|
|
120
|
+
"""
|
|
121
|
+
Create a default configuration with basic commands.
|
|
122
|
+
|
|
123
|
+
This provides a sensible starting point for most bots.
|
|
124
|
+
"""
|
|
125
|
+
return CmdsConfig(
|
|
126
|
+
languages=languages,
|
|
127
|
+
fallback_language=languages[0] if languages else "en",
|
|
128
|
+
i18n_key_prefix="cmd",
|
|
129
|
+
# Basic commands that most bots need
|
|
130
|
+
commands={
|
|
131
|
+
"start": CommandDef(i18n_key="start.desc"),
|
|
132
|
+
"help": CommandDef(i18n_key="help.desc"),
|
|
133
|
+
"cancel": CommandDef(i18n_key="cancel.desc"),
|
|
134
|
+
},
|
|
135
|
+
# Simple profile structure
|
|
136
|
+
profiles={
|
|
137
|
+
"guest": ProfileDef(include=["start", "help"]),
|
|
138
|
+
"user": ProfileDef(include=["start", "help", "cancel"]),
|
|
139
|
+
},
|
|
140
|
+
# Default scope for all private chats
|
|
141
|
+
scopes=[
|
|
142
|
+
ScopeDef(scope="all_private_chats", profile="guest"),
|
|
143
|
+
],
|
|
144
|
+
# Menu button
|
|
145
|
+
menu_button=MenuButtonDef(mode="commands"),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def create_default_profile_resolver() -> ProfileResolver:
|
|
150
|
+
"""
|
|
151
|
+
Create a default profile resolver that maps user flags to profiles.
|
|
152
|
+
|
|
153
|
+
This provides a simple guest/user distinction based on registration status.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def default_resolver(flags: Flags) -> str:
|
|
157
|
+
if flags.is_during_registration or not flags.is_registered:
|
|
158
|
+
return "guest"
|
|
159
|
+
return "user"
|
|
160
|
+
|
|
161
|
+
return default_resolver
|
aiogram_cmds/builder.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command building - construct BotCommand objects from names and i18n/inlined descriptions.
|
|
3
|
+
|
|
4
|
+
- Validates command names per Telegram constraints.
|
|
5
|
+
- Resolves descriptions via translator or falls back to Title Case.
|
|
6
|
+
- Enforces Telegram length limits (name: 1..32, description: 1..256).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from aiogram.types import BotCommand
|
|
13
|
+
|
|
14
|
+
from .translator import Translator
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Telegram /command rules (lowercase letters, digits, underscores; must start with a letter)
|
|
19
|
+
_CMD_RE = re.compile(r"^[a-z][a-z0-9_]{0,31}$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def validate_name(name: str) -> str:
|
|
23
|
+
"""Coerce a human/provided name into a safe Telegram command name."""
|
|
24
|
+
n = (name or "").strip().lower().replace(" ", "_")
|
|
25
|
+
if not _CMD_RE.match(n):
|
|
26
|
+
logger.debug("Invalid command name '%s', coercing to safe form", name)
|
|
27
|
+
# drop leading non-letters
|
|
28
|
+
while n and not n[0].isalpha():
|
|
29
|
+
n = n[1:]
|
|
30
|
+
# remove invalid chars
|
|
31
|
+
n = re.sub(r"[^a-z0-9_]", "", n)
|
|
32
|
+
# clamp length
|
|
33
|
+
n = n[:32] if n else "cmd"
|
|
34
|
+
if not _CMD_RE.match(n):
|
|
35
|
+
n = "cmd"
|
|
36
|
+
return n
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_bot_commands(
|
|
40
|
+
command_names: list[str],
|
|
41
|
+
*,
|
|
42
|
+
lang: str,
|
|
43
|
+
translator: Translator | None = None,
|
|
44
|
+
key_prefix: str = "cmd",
|
|
45
|
+
) -> list[BotCommand]:
|
|
46
|
+
"""
|
|
47
|
+
Build BotCommand objects from command names using i18n descriptions.
|
|
48
|
+
|
|
49
|
+
i18n key pattern by default: {key_prefix}.{name}.desc
|
|
50
|
+
"""
|
|
51
|
+
cmds: list[BotCommand] = []
|
|
52
|
+
|
|
53
|
+
for raw in command_names:
|
|
54
|
+
name = validate_name(raw)
|
|
55
|
+
|
|
56
|
+
# Try to get description from i18n
|
|
57
|
+
desc = None
|
|
58
|
+
if translator:
|
|
59
|
+
key = f"{key_prefix}.{name}.desc"
|
|
60
|
+
try:
|
|
61
|
+
desc = translator(key, locale=lang)
|
|
62
|
+
except Exception: # pragma: no cover
|
|
63
|
+
logger.debug(
|
|
64
|
+
"Translator failed for key %s (lang=%s)", key, lang, exc_info=True
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Fallback if missing translation
|
|
68
|
+
if not desc:
|
|
69
|
+
desc = name.replace("_", " ").title()
|
|
70
|
+
logger.debug(
|
|
71
|
+
"Missing i18n for %s.%s, fallback to '%s'", key_prefix, name, desc
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
cmds.append(BotCommand(command=name[:32], description=desc[:256]))
|
|
75
|
+
|
|
76
|
+
logger.debug("Built %d commands for language '%s'", len(cmds), lang)
|
|
77
|
+
return cmds
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command builders for config-driven mode.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from aiogram.types import BotCommand
|
|
8
|
+
|
|
9
|
+
from .builder import validate_name
|
|
10
|
+
from .registry import CommandRegistry
|
|
11
|
+
from .translator import Translator
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_from_config(
|
|
17
|
+
reg: CommandRegistry,
|
|
18
|
+
names: list[str],
|
|
19
|
+
*,
|
|
20
|
+
lang: str,
|
|
21
|
+
translator: Translator | None,
|
|
22
|
+
key_prefix: str,
|
|
23
|
+
) -> list[BotCommand]:
|
|
24
|
+
"""
|
|
25
|
+
Build commands using CmdsConfig registry:
|
|
26
|
+
- inline descriptions win
|
|
27
|
+
- else use command.i18n_key (relative to prefix unless dotted)
|
|
28
|
+
- else fallback to {prefix}.{name}.desc
|
|
29
|
+
- else Title Case fallback
|
|
30
|
+
"""
|
|
31
|
+
items: list[BotCommand] = []
|
|
32
|
+
for raw in names:
|
|
33
|
+
name = validate_name(raw)
|
|
34
|
+
desc = None
|
|
35
|
+
c = reg.get_command(name)
|
|
36
|
+
|
|
37
|
+
# 1) inline descriptions
|
|
38
|
+
if c and c.descriptions:
|
|
39
|
+
desc = c.descriptions.get(lang)
|
|
40
|
+
|
|
41
|
+
# 2) specified i18n key
|
|
42
|
+
if not desc and c and c.i18n_key and translator:
|
|
43
|
+
i18n_key = c.i18n_key if "." in c.i18n_key else f"{key_prefix}.{c.i18n_key}"
|
|
44
|
+
try:
|
|
45
|
+
desc = translator(i18n_key, locale=lang)
|
|
46
|
+
except Exception:
|
|
47
|
+
logger.debug(
|
|
48
|
+
"Translator failed for key %s (lang=%s)",
|
|
49
|
+
i18n_key,
|
|
50
|
+
lang,
|
|
51
|
+
exc_info=True,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# 3) default i18n key {prefix}.{name}.desc
|
|
55
|
+
if not desc and translator:
|
|
56
|
+
key = f"{key_prefix}.{name}.desc"
|
|
57
|
+
try:
|
|
58
|
+
desc = translator(key, locale=lang)
|
|
59
|
+
except Exception:
|
|
60
|
+
logger.debug(
|
|
61
|
+
"Translator failed for key %s (lang=%s)", key, lang, exc_info=True
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# 4) fallback
|
|
65
|
+
if not desc:
|
|
66
|
+
desc = name.replace("_", " ").title()
|
|
67
|
+
|
|
68
|
+
items.append(BotCommand(command=name[:32], description=desc[:256]))
|
|
69
|
+
return items
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Full customizable configuration models for aiogram_cmds.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
+
|
|
11
|
+
ScopeType = Literal[
|
|
12
|
+
"default",
|
|
13
|
+
"all_private_chats",
|
|
14
|
+
"all_group_chats",
|
|
15
|
+
"all_chat_admins",
|
|
16
|
+
"chat",
|
|
17
|
+
"chat_admins",
|
|
18
|
+
"chat_member",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CommandDef(BaseModel):
|
|
23
|
+
model_config = ConfigDict(extra="ignore")
|
|
24
|
+
# Either use i18n_key OR inline descriptions per language
|
|
25
|
+
i18n_key: str | None = None
|
|
26
|
+
descriptions: dict[str, str] = Field(default_factory=dict)
|
|
27
|
+
tags: list[str] = Field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ProfileDef(BaseModel):
|
|
31
|
+
model_config = ConfigDict(extra="ignore")
|
|
32
|
+
include: list[str] = Field(default_factory=list)
|
|
33
|
+
exclude: list[str] = Field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ScopeDef(BaseModel):
|
|
37
|
+
model_config = ConfigDict(extra="ignore")
|
|
38
|
+
scope: ScopeType
|
|
39
|
+
profile: str
|
|
40
|
+
chat_id: int | None = None
|
|
41
|
+
user_id: int | None = None
|
|
42
|
+
languages: list[str] = Field(default_factory=list) # empty = use global languages
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MenuButtonDef(BaseModel):
|
|
46
|
+
model_config = ConfigDict(extra="ignore")
|
|
47
|
+
mode: Literal["commands", "default"] = "commands"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CmdsConfig(BaseModel):
|
|
51
|
+
"""
|
|
52
|
+
Full customizable configuration:
|
|
53
|
+
- languages, fallback language, i18n key prefix
|
|
54
|
+
- commands registry (with i18n_key or inline descriptions)
|
|
55
|
+
- profiles: named sets of commands (include/exclude)
|
|
56
|
+
- scopes: where to apply which profile
|
|
57
|
+
- menu button config
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
model_config = ConfigDict(extra="ignore")
|
|
61
|
+
languages: list[str] = ["en"]
|
|
62
|
+
fallback_language: str = "en"
|
|
63
|
+
i18n_key_prefix: str = "cmd"
|
|
64
|
+
commands: dict[str, CommandDef] = Field(default_factory=dict)
|
|
65
|
+
profiles: dict[str, ProfileDef] = Field(default_factory=dict)
|
|
66
|
+
scopes: list[ScopeDef] = Field(default_factory=list)
|
|
67
|
+
menu_button: MenuButtonDef = Field(default_factory=MenuButtonDef)
|
aiogram_cmds/dynamic.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dynamic command management for runtime profile changes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from aiogram import Bot
|
|
6
|
+
from aiogram.types import BotCommandScopeChat, BotCommandScopeChatMember
|
|
7
|
+
|
|
8
|
+
from .builders_configured import build_from_config
|
|
9
|
+
from .customize import CmdsConfig
|
|
10
|
+
from .registry import CommandRegistry
|
|
11
|
+
from .translator import Translator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def set_user_profile(
|
|
15
|
+
bot: Bot,
|
|
16
|
+
cfg: CmdsConfig,
|
|
17
|
+
*,
|
|
18
|
+
translator: Translator | None,
|
|
19
|
+
user_id: int,
|
|
20
|
+
chat_id: int | None,
|
|
21
|
+
profile: str,
|
|
22
|
+
language: str,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Dynamically apply a profile to a user (optionally in a specific chat).
|
|
26
|
+
"""
|
|
27
|
+
reg = CommandRegistry(cfg)
|
|
28
|
+
names = list(reg.resolve_profile_commands(profile))
|
|
29
|
+
cmds = build_from_config(
|
|
30
|
+
reg, names, lang=language, translator=translator, key_prefix=cfg.i18n_key_prefix
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
scope = (
|
|
34
|
+
BotCommandScopeChat(chat_id=user_id)
|
|
35
|
+
if chat_id is None
|
|
36
|
+
else BotCommandScopeChatMember(chat_id=chat_id, user_id=user_id)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
await bot.set_my_commands(cmds, scope=scope, language_code=language)
|
aiogram_cmds/manager.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-level manager that supports:
|
|
3
|
+
1) Simple mode (CmdsSettings + CommandPolicy -> list of command names)
|
|
4
|
+
2) Full customizable mode (CmdsConfig + ProfileResolver -> profiles & scopes)
|
|
5
|
+
|
|
6
|
+
Use whichever you provide. If both are provided, config mode is used for setup_all(),
|
|
7
|
+
and update_user_commands() will prefer the profile resolver if present; otherwise falls
|
|
8
|
+
back to policy-based names.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import TYPE_CHECKING, Optional
|
|
13
|
+
|
|
14
|
+
from aiogram import Bot
|
|
15
|
+
|
|
16
|
+
from .apply import apply_config
|
|
17
|
+
from .dynamic import set_user_profile
|
|
18
|
+
from .policy import CommandPolicy, Flags, ProfileResolver, default_policy
|
|
19
|
+
from .setters import (
|
|
20
|
+
set_user_commands_by_flags,
|
|
21
|
+
setup_all_command_scopes,
|
|
22
|
+
)
|
|
23
|
+
from .settings import CmdsSettings
|
|
24
|
+
from .translator import Translator
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from .customize import CmdsConfig
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CommandScopeManager:
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
bot: Bot,
|
|
36
|
+
*,
|
|
37
|
+
settings: CmdsSettings | None = None,
|
|
38
|
+
config: Optional["CmdsConfig"] = None,
|
|
39
|
+
translator: Translator | None = None,
|
|
40
|
+
policy: CommandPolicy | None = None,
|
|
41
|
+
profile_resolver: ProfileResolver | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
self.bot = bot
|
|
44
|
+
self.settings = settings or CmdsSettings()
|
|
45
|
+
self.config = config
|
|
46
|
+
self.translator = translator
|
|
47
|
+
self.policy = policy or default_policy
|
|
48
|
+
self.profile_resolver = profile_resolver
|
|
49
|
+
|
|
50
|
+
async def update_user_commands(
|
|
51
|
+
self,
|
|
52
|
+
user_id: int,
|
|
53
|
+
is_registered: bool,
|
|
54
|
+
has_vehicle: bool,
|
|
55
|
+
is_during_registration: bool = False,
|
|
56
|
+
user_language: str = "en",
|
|
57
|
+
chat_id: int | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
flags = Flags(
|
|
60
|
+
is_registered=is_registered,
|
|
61
|
+
has_vehicle=has_vehicle,
|
|
62
|
+
is_during_registration=is_during_registration,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
if self.config and self.profile_resolver:
|
|
67
|
+
profile = self.profile_resolver(flags)
|
|
68
|
+
await set_user_profile(
|
|
69
|
+
self.bot,
|
|
70
|
+
self.config,
|
|
71
|
+
translator=self.translator,
|
|
72
|
+
user_id=user_id,
|
|
73
|
+
chat_id=chat_id,
|
|
74
|
+
profile=profile,
|
|
75
|
+
language=user_language,
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
# Classic mode: compute names via policy
|
|
79
|
+
await set_user_commands_by_flags(
|
|
80
|
+
self.bot,
|
|
81
|
+
user_id,
|
|
82
|
+
is_registered=is_registered,
|
|
83
|
+
has_vehicle=has_vehicle,
|
|
84
|
+
is_during_registration=is_during_registration,
|
|
85
|
+
user_language=user_language,
|
|
86
|
+
policy=self.policy,
|
|
87
|
+
translator=self.translator,
|
|
88
|
+
key_prefix=self.settings.i18n_key_prefix,
|
|
89
|
+
)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.error(
|
|
92
|
+
"Failed to update user commands for user %d: %s",
|
|
93
|
+
user_id,
|
|
94
|
+
e,
|
|
95
|
+
exc_info=True,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Human-friendly log
|
|
99
|
+
if is_during_registration:
|
|
100
|
+
status = "registration"
|
|
101
|
+
elif is_registered and has_vehicle:
|
|
102
|
+
status = "onboarded"
|
|
103
|
+
elif is_registered and not has_vehicle:
|
|
104
|
+
status = "registered_no_vehicle"
|
|
105
|
+
else:
|
|
106
|
+
status = "guest"
|
|
107
|
+
|
|
108
|
+
logger.info(
|
|
109
|
+
"Updated commands for user %s: %s (registered=%s, vehicle=%s, during_registration=%s)",
|
|
110
|
+
user_id,
|
|
111
|
+
status,
|
|
112
|
+
is_registered,
|
|
113
|
+
has_vehicle,
|
|
114
|
+
is_during_registration,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
async def clear_user_commands(self, user_id: int) -> None:
|
|
118
|
+
from aiogram.types import BotCommandScopeChat
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
await self.bot.delete_my_commands(
|
|
122
|
+
scope=BotCommandScopeChat(chat_id=user_id)
|
|
123
|
+
)
|
|
124
|
+
logger.info("Cleared commands for user %s", user_id)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.error(
|
|
127
|
+
"Failed to clear commands for user %d: %s", user_id, e, exc_info=True
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
async def setup_all(self) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Setup all command scopes + menu button.
|
|
133
|
+
- If a full CmdsConfig is provided: apply_config (full customizable mode)
|
|
134
|
+
- Else: setup_all_command_scopes (classic mode)
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
if self.config:
|
|
138
|
+
await apply_config(self.bot, self.config, translator=self.translator)
|
|
139
|
+
else:
|
|
140
|
+
await setup_all_command_scopes(
|
|
141
|
+
self.bot,
|
|
142
|
+
languages=tuple(self.settings.languages),
|
|
143
|
+
translator=self.translator,
|
|
144
|
+
key_prefix=self.settings.i18n_key_prefix,
|
|
145
|
+
menu_button=self.settings.menu_button,
|
|
146
|
+
)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.error("Failed to setup all commands: %s", e, exc_info=True)
|
aiogram_cmds/policy.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Policies & flags:
|
|
3
|
+
- CommandPolicy: returns a list of command names from user Flags.
|
|
4
|
+
- ProfileResolver: returns a profile name (for config-driven mode).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Protocol
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class Flags:
|
|
13
|
+
"""User flags that determine which commands are available."""
|
|
14
|
+
|
|
15
|
+
is_registered: bool
|
|
16
|
+
has_vehicle: bool
|
|
17
|
+
is_during_registration: bool = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CommandPolicy(Protocol):
|
|
21
|
+
"""Protocol for command policy functions."""
|
|
22
|
+
|
|
23
|
+
def __call__(self, flags: Flags) -> list[str]: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ProfileResolver(Protocol):
|
|
27
|
+
"""Resolve a profile name for a given user Flags (for CmdsConfig mode)."""
|
|
28
|
+
|
|
29
|
+
def __call__(self, flags: Flags) -> str: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def default_policy(flags: Flags) -> list[str]:
|
|
33
|
+
"""
|
|
34
|
+
Minimal, project-agnostic command policy.
|
|
35
|
+
"""
|
|
36
|
+
if flags.is_during_registration or not flags.is_registered:
|
|
37
|
+
return ["start", "cancel"]
|
|
38
|
+
return ["start", "help", "cancel", "profile", "settings"]
|
aiogram_cmds/registry.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command registry for managing profiles and commands in CmdsConfig.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .customize import CmdsConfig, CommandDef, ProfileDef
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CommandRegistry:
|
|
9
|
+
def __init__(self, cfg: CmdsConfig):
|
|
10
|
+
self.cfg = cfg
|
|
11
|
+
|
|
12
|
+
def resolve_profile_commands(self, profile: str) -> list[str]:
|
|
13
|
+
p: ProfileDef | None = self.cfg.profiles.get(profile)
|
|
14
|
+
if not p:
|
|
15
|
+
return []
|
|
16
|
+
keep = [n for n in p.include if n in self.cfg.commands]
|
|
17
|
+
exclude = set(p.exclude)
|
|
18
|
+
return [n for n in keep if n not in exclude]
|
|
19
|
+
|
|
20
|
+
def get_command(self, name: str) -> CommandDef | None:
|
|
21
|
+
return self.cfg.commands.get(name)
|
aiogram_cmds/setters.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple (classic) setters for per-user and global scopes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
|
|
8
|
+
from aiogram import Bot
|
|
9
|
+
from aiogram.types import (
|
|
10
|
+
BotCommandScopeAllPrivateChats,
|
|
11
|
+
BotCommandScopeChat,
|
|
12
|
+
BotCommandScopeDefault,
|
|
13
|
+
MenuButtonCommands,
|
|
14
|
+
MenuButtonDefault,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .builder import build_bot_commands
|
|
18
|
+
from .policy import CommandPolicy, Flags, default_policy
|
|
19
|
+
from .translator import Translator
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def set_user_commands_by_flags(
|
|
25
|
+
bot: Bot,
|
|
26
|
+
user_id: int,
|
|
27
|
+
*,
|
|
28
|
+
is_registered: bool,
|
|
29
|
+
has_vehicle: bool,
|
|
30
|
+
is_during_registration: bool = False,
|
|
31
|
+
user_language: str = "en",
|
|
32
|
+
policy: CommandPolicy = default_policy,
|
|
33
|
+
translator: Translator | None = None,
|
|
34
|
+
key_prefix: str = "cmd",
|
|
35
|
+
) -> None:
|
|
36
|
+
names = policy(
|
|
37
|
+
Flags(
|
|
38
|
+
is_registered=is_registered,
|
|
39
|
+
has_vehicle=has_vehicle,
|
|
40
|
+
is_during_registration=is_during_registration,
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
commands = build_bot_commands(
|
|
44
|
+
names, lang=user_language, translator=translator, key_prefix=key_prefix
|
|
45
|
+
)
|
|
46
|
+
# Per-chat override (user's private chat)
|
|
47
|
+
await bot.set_my_commands(
|
|
48
|
+
commands=commands,
|
|
49
|
+
scope=BotCommandScopeChat(chat_id=user_id),
|
|
50
|
+
language_code=user_language,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def clear_user_commands(bot: Bot, user_id: int) -> None:
|
|
55
|
+
# Delete per-chat override (falls back to broader scopes)
|
|
56
|
+
await bot.delete_my_commands(scope=BotCommandScopeChat(chat_id=user_id))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def setup_all_command_scopes(
|
|
60
|
+
bot: Bot,
|
|
61
|
+
*,
|
|
62
|
+
languages: tuple[str, ...],
|
|
63
|
+
translator: Translator | None = None,
|
|
64
|
+
key_prefix: str = "cmd",
|
|
65
|
+
menu_button: str = "commands",
|
|
66
|
+
default_commands: Iterable[str] | None = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Configure default/global command sets and the menu button for classic mode.
|
|
70
|
+
"""
|
|
71
|
+
base_names = list(default_commands or ["start", "help"])
|
|
72
|
+
|
|
73
|
+
# default scope without language as a fallback
|
|
74
|
+
fallback_cmds = build_bot_commands(
|
|
75
|
+
base_names, lang=languages[0], translator=translator, key_prefix=key_prefix
|
|
76
|
+
)
|
|
77
|
+
await bot.set_my_commands(fallback_cmds, scope=BotCommandScopeDefault())
|
|
78
|
+
|
|
79
|
+
# language-specific sets for private chats (most common case)
|
|
80
|
+
for lang in languages:
|
|
81
|
+
cmds = build_bot_commands(
|
|
82
|
+
base_names, lang=lang, translator=translator, key_prefix=key_prefix
|
|
83
|
+
)
|
|
84
|
+
await bot.set_my_commands(
|
|
85
|
+
cmds, scope=BotCommandScopeAllPrivateChats(), language_code=lang
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Menu button setup
|
|
89
|
+
if menu_button == "commands":
|
|
90
|
+
await bot.set_chat_menu_button(menu_button=MenuButtonCommands())
|
|
91
|
+
else:
|
|
92
|
+
await bot.set_chat_menu_button(menu_button=MenuButtonDefault())
|
aiogram_cmds/settings.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Standalone settings for aiogram_cmds package.
|
|
3
|
+
|
|
4
|
+
This module provides settings dataclass and configuration loading
|
|
5
|
+
from pyproject.toml, making the library configurable without hardcoded values.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import tomllib
|
|
14
|
+
from pydantic import BaseModel, ConfigDict, field_validator
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CmdsSettings(BaseModel):
|
|
18
|
+
"""Configuration settings for command management."""
|
|
19
|
+
|
|
20
|
+
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
21
|
+
languages: tuple[str, ...] = ("en",)
|
|
22
|
+
fallback_language: str = "en"
|
|
23
|
+
i18n_key_prefix: str = "cmd"
|
|
24
|
+
profile: str = "default"
|
|
25
|
+
menu_button: str = "commands" # "commands" | "default"
|
|
26
|
+
|
|
27
|
+
@field_validator("languages", mode="before")
|
|
28
|
+
@classmethod
|
|
29
|
+
def _ensure_nonempty(cls, v):
|
|
30
|
+
if v is None:
|
|
31
|
+
return ("en",)
|
|
32
|
+
if isinstance(v, str):
|
|
33
|
+
return (v,)
|
|
34
|
+
if isinstance(v, (list, tuple)):
|
|
35
|
+
return tuple(v) if v else ("en",)
|
|
36
|
+
return ("en",)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_settings(pyproject_path: Path | None = None) -> CmdsSettings:
|
|
40
|
+
"""
|
|
41
|
+
Load settings from [tool.aiogram_cmds] in pyproject.toml (if present).
|
|
42
|
+
"""
|
|
43
|
+
p = pyproject_path or Path("pyproject.toml")
|
|
44
|
+
if not p.exists():
|
|
45
|
+
return CmdsSettings()
|
|
46
|
+
try:
|
|
47
|
+
data = tomllib.loads(p.read_text(encoding="utf-8"))
|
|
48
|
+
cfg = (data.get("tool") or {}).get("aiogram_cmds") or {}
|
|
49
|
+
return CmdsSettings.model_validate(cfg)
|
|
50
|
+
except Exception as ex: # pragma: no cover
|
|
51
|
+
logging.getLogger(__name__).warning(
|
|
52
|
+
"aiogram_cmds: failed to load settings: %s", ex
|
|
53
|
+
)
|
|
54
|
+
return CmdsSettings()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Translator protocol and adapters for i18n integration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Translator(Protocol):
|
|
9
|
+
"""Protocol for translation functions."""
|
|
10
|
+
|
|
11
|
+
def __call__(self, key: str, *, locale: str) -> str | None: ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_translator_from_i18n(i18n_obj) -> Translator:
|
|
15
|
+
"""
|
|
16
|
+
Build translator adapter from an aiogram i18n instance exposing gettext(key, locale=?).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def _t(key: str, *, locale: str) -> str | None:
|
|
20
|
+
try:
|
|
21
|
+
value = i18n_obj.gettext(key, locale=locale)
|
|
22
|
+
if not value or value == key:
|
|
23
|
+
return None
|
|
24
|
+
return value
|
|
25
|
+
except Exception:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
return _t
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def noop_translator(key: str, *, locale: str) -> str | None:
|
|
32
|
+
"""No-op translator that always returns None."""
|
|
33
|
+
return None
|
aiogram_cmds/version.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aiogram-cmds
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command management library for aiogram v3
|
|
5
|
+
Project-URL: Homepage, https://github.com/ArmanAvanesyan/aiogram-cmds
|
|
6
|
+
Project-URL: Repository, https://github.com/ArmanAvanesyan/aiogram-cmds
|
|
7
|
+
Project-URL: Issues, https://github.com/ArmanAvanesyan/aiogram-cmds/issues
|
|
8
|
+
Author: aiogram-cmds contributors
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Communications :: Chat
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: aiogram>=3.0.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: bandit>=1.7.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: build>=1.0.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: isort>=5.12.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pip-audit>=2.0.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pyright>=1.1.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-benchmark>=4.0.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-mock>=3.10.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: twine>=5.0.0; extra == 'dev'
|
|
39
|
+
Provides-Extra: redis
|
|
40
|
+
Requires-Dist: redis>=4.0.0; extra == 'redis'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# aiogram-cmds
|
|
44
|
+
|
|
45
|
+
[](https://github.com/ArmanAvanesyan/aiogram-cmds/actions)
|
|
46
|
+
[](https://pypi.org/project/aiogram-cmds/)
|
|
47
|
+
[](https://pypi.org/project/aiogram-cmds/)
|
|
48
|
+
[](https://pypi.org/project/aiogram-cmds/)
|
|
49
|
+
|
|
50
|
+
**Command management library for aiogram v3** - Manage Telegram bot commands with i18n support, user profiles, and dynamic scopes.
|
|
51
|
+
|
|
52
|
+
## ✨ Features
|
|
53
|
+
|
|
54
|
+
* **Simple & Advanced Modes**: Start simple, scale to complex configurations
|
|
55
|
+
* **i18n Integration**: Built-in support for aiogram i18n with fallback handling
|
|
56
|
+
* **User Profiles**: Dynamic command sets based on user status (guest, registered, etc.)
|
|
57
|
+
* **Multiple Scopes**: Support for all Telegram command scopes (private, group, admin, etc.)
|
|
58
|
+
* **Auto-Setup**: One-call setup with sensible defaults
|
|
59
|
+
* **Type-Safe**: Full type hints and strict type checking
|
|
60
|
+
* **Production Ready**: Comprehensive testing, CI/CD, and documentation
|
|
61
|
+
|
|
62
|
+
## 📦 Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Basic installation
|
|
66
|
+
pip install aiogram-cmds
|
|
67
|
+
|
|
68
|
+
# With Redis support (for multi-worker deployments)
|
|
69
|
+
pip install aiogram-cmds[redis]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## ⚡ Quick Start
|
|
73
|
+
|
|
74
|
+
### Auto-Setup (Recommended)
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from aiogram import Bot, Dispatcher
|
|
78
|
+
from aiogram_cmds.auto_setup import setup_commands_auto
|
|
79
|
+
|
|
80
|
+
async def on_startup():
|
|
81
|
+
# Automatically set up commands with sensible defaults
|
|
82
|
+
command_manager = await setup_commands_auto(bot)
|
|
83
|
+
# Commands are ready to use!
|
|
84
|
+
|
|
85
|
+
# Your handlers
|
|
86
|
+
@dp.message(Command("start"))
|
|
87
|
+
async def handle_start(message: Message):
|
|
88
|
+
await message.answer("Welcome! Use /help to see available commands.")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Simple Mode
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from aiogram import Bot, Dispatcher
|
|
95
|
+
from aiogram_cmds import CommandScopeManager, load_settings, build_translator_from_i18n
|
|
96
|
+
|
|
97
|
+
# Load settings from pyproject.toml or use defaults
|
|
98
|
+
settings = load_settings()
|
|
99
|
+
translator = build_translator_from_i18n(i18n) # Your aiogram i18n instance
|
|
100
|
+
|
|
101
|
+
# Create manager
|
|
102
|
+
manager = CommandScopeManager(bot, settings=settings, translator=translator)
|
|
103
|
+
|
|
104
|
+
# Set up all command scopes
|
|
105
|
+
await manager.setup_all()
|
|
106
|
+
|
|
107
|
+
# Update user commands dynamically
|
|
108
|
+
await manager.update_user_commands(
|
|
109
|
+
user_id=12345,
|
|
110
|
+
is_registered=True,
|
|
111
|
+
has_vehicle=False,
|
|
112
|
+
user_language="en"
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Advanced Mode
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from aiogram_cmds import CmdsConfig, CommandDef, ProfileDef, ScopeDef, MenuButtonDef
|
|
120
|
+
|
|
121
|
+
# Define your command configuration
|
|
122
|
+
config = CmdsConfig(
|
|
123
|
+
languages=["en", "ru", "es"],
|
|
124
|
+
commands={
|
|
125
|
+
"start": CommandDef(i18n_key="start.desc"),
|
|
126
|
+
"help": CommandDef(i18n_key="help.desc"),
|
|
127
|
+
"profile": CommandDef(i18n_key="profile.desc"),
|
|
128
|
+
"admin": CommandDef(i18n_key="admin.desc"),
|
|
129
|
+
},
|
|
130
|
+
profiles={
|
|
131
|
+
"guest": ProfileDef(include=["start", "help"]),
|
|
132
|
+
"user": ProfileDef(include=["start", "help", "profile"]),
|
|
133
|
+
"admin": ProfileDef(include=["start", "help", "profile", "admin"]),
|
|
134
|
+
},
|
|
135
|
+
scopes=[
|
|
136
|
+
ScopeDef(scope="all_private_chats", profile="guest"),
|
|
137
|
+
ScopeDef(scope="chat", chat_id=12345, profile="admin"),
|
|
138
|
+
],
|
|
139
|
+
menu_button=MenuButtonDef(mode="commands"),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Create manager with custom profile resolver
|
|
143
|
+
def my_profile_resolver(flags):
|
|
144
|
+
if flags.is_admin:
|
|
145
|
+
return "admin"
|
|
146
|
+
elif flags.is_registered:
|
|
147
|
+
return "user"
|
|
148
|
+
return "guest"
|
|
149
|
+
|
|
150
|
+
manager = CommandScopeManager(
|
|
151
|
+
bot,
|
|
152
|
+
config=config,
|
|
153
|
+
profile_resolver=my_profile_resolver
|
|
154
|
+
)
|
|
155
|
+
await manager.setup_all()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## 🎯 Use Cases
|
|
159
|
+
|
|
160
|
+
### E-commerce Bot
|
|
161
|
+
```python
|
|
162
|
+
# Different commands for different user states
|
|
163
|
+
profiles = {
|
|
164
|
+
"visitor": ProfileDef(include=["start", "catalog", "help"]),
|
|
165
|
+
"customer": ProfileDef(include=["start", "catalog", "cart", "orders", "help"]),
|
|
166
|
+
"vip": ProfileDef(include=["start", "catalog", "cart", "orders", "vip_offers", "help"]),
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Admin Bot
|
|
171
|
+
```python
|
|
172
|
+
# Admin commands only for specific users
|
|
173
|
+
scopes = [
|
|
174
|
+
ScopeDef(scope="all_private_chats", profile="user"),
|
|
175
|
+
ScopeDef(scope="chat_member", chat_id=12345, user_id=67890, profile="admin"),
|
|
176
|
+
]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Multi-Language Bot
|
|
180
|
+
```python
|
|
181
|
+
# Commands in multiple languages
|
|
182
|
+
config = CmdsConfig(
|
|
183
|
+
languages=["en", "ru", "es", "fr"],
|
|
184
|
+
commands={
|
|
185
|
+
"start": CommandDef(i18n_key="commands.start"),
|
|
186
|
+
"help": CommandDef(i18n_key="commands.help"),
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## 📚 Documentation
|
|
192
|
+
|
|
193
|
+
👉 **[Full Documentation](https://aiogram-cmds.dev)** ← Start here!
|
|
194
|
+
|
|
195
|
+
* **[Quickstart](https://aiogram-cmds.dev/quickstart/)** - Get started in 5 minutes
|
|
196
|
+
* **[Configuration](https://aiogram-cmds.dev/configuration/)** - Complete configuration guide
|
|
197
|
+
* **[API Reference](https://aiogram-cmds.dev/api/)** - Full API documentation
|
|
198
|
+
* **[Tutorials](https://aiogram-cmds.dev/tutorials/)** - Step-by-step guides
|
|
199
|
+
* **[Examples](https://aiogram-cmds.dev/examples/)** - Complete working examples
|
|
200
|
+
|
|
201
|
+
## 🏗️ Architecture
|
|
202
|
+
|
|
203
|
+
aiogram-cmds provides three levels of complexity:
|
|
204
|
+
|
|
205
|
+
1. **Auto-Setup**: Zero configuration, works out of the box
|
|
206
|
+
2. **Simple Mode**: Settings-based configuration with policies
|
|
207
|
+
3. **Advanced Mode**: Full control with profiles, scopes, and custom resolvers
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
211
|
+
│ Auto-Setup │ │ Simple Mode │ │ Advanced Mode │
|
|
212
|
+
│ │ │ │ │ │
|
|
213
|
+
│ setup_commands_ │ │ CmdsSettings + │ │ CmdsConfig + │
|
|
214
|
+
│ auto() │ │ CommandPolicy │ │ ProfileResolver │
|
|
215
|
+
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
|
216
|
+
│ │ │
|
|
217
|
+
└───────────────────────┼───────────────────────┘
|
|
218
|
+
│
|
|
219
|
+
┌──────────────────┐
|
|
220
|
+
│ CommandScopeMgr │
|
|
221
|
+
│ │
|
|
222
|
+
│ • setup_all() │
|
|
223
|
+
│ • update_user() │
|
|
224
|
+
│ • clear_user() │
|
|
225
|
+
└──────────────────┘
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## 🤝 Contributing
|
|
229
|
+
|
|
230
|
+
We welcome contributions! See our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
231
|
+
|
|
232
|
+
### Development Setup
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
# Clone the repository
|
|
236
|
+
git clone https://github.com/ArmanAvanesyan/aiogram-cmds.git
|
|
237
|
+
cd aiogram-cmds
|
|
238
|
+
|
|
239
|
+
# Set up development environment
|
|
240
|
+
uv venv
|
|
241
|
+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
242
|
+
uv pip install -e ".[dev]"
|
|
243
|
+
|
|
244
|
+
# Run tests
|
|
245
|
+
uv run pytest
|
|
246
|
+
|
|
247
|
+
# Run linting
|
|
248
|
+
uv run ruff check .
|
|
249
|
+
uv run ruff format .
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## 💬 Community & Support
|
|
253
|
+
|
|
254
|
+
* 💬 **[Discussions](https://github.com/ArmanAvanesyan/aiogram-cmds/discussions)** - Questions, ideas, and community chat
|
|
255
|
+
* 🐛 **[Issues](https://github.com/ArmanAvanesyan/aiogram-cmds/issues)** - Bug reports and feature requests
|
|
256
|
+
* 📖 **[Documentation](https://aiogram-cmds.dev)** - Complete guides and API reference
|
|
257
|
+
|
|
258
|
+
## 🔒 Security
|
|
259
|
+
|
|
260
|
+
For security issues, see [SECURITY.md](SECURITY.md).
|
|
261
|
+
|
|
262
|
+
## 📄 License
|
|
263
|
+
|
|
264
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
265
|
+
|
|
266
|
+
## 🙏 Acknowledgments
|
|
267
|
+
|
|
268
|
+
* Built for [aiogram v3](https://github.com/aiogram/aiogram) - Modern Telegram Bot API framework
|
|
269
|
+
* Inspired by the need for robust command management in production environments
|
|
270
|
+
|
|
271
|
+
## 📊 Project Status
|
|
272
|
+
|
|
273
|
+
- ✅ **Core Features**: Command management, i18n, profiles, scopes
|
|
274
|
+
- ✅ **Documentation**: Comprehensive guides and API reference
|
|
275
|
+
- ✅ **Testing**: Unit, integration, and performance tests
|
|
276
|
+
- ✅ **CI/CD**: Automated testing and deployment
|
|
277
|
+
- 🚧 **Examples**: Working bot examples (in progress)
|
|
278
|
+
- 🔮 **Future**: Rate limiting, caching, advanced analytics
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
**Made with ❤️ for the aiogram community**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
aiogram_cmds/__init__.py,sha256=DGYWAquSElFPKqOyqQd8VVjAKwVviC_1dZ4uuT0qmrs,2240
|
|
2
|
+
aiogram_cmds/apply.py,sha256=FwtmZD-xxnn_-otfMYZfqPoRqbaQDaCx0p8edBCh54Q,2662
|
|
3
|
+
aiogram_cmds/auto_setup.py,sha256=OC2oCW3Y8bh30cA2-grVtidLWjhrnFKG3j1tQS-kXOA,4921
|
|
4
|
+
aiogram_cmds/builder.py,sha256=ZlCJhpc2GPFcfmrqCz7aUfggMwjmB0X4lVUxJ5BywsI,2347
|
|
5
|
+
aiogram_cmds/builders_configured.py,sha256=sihthu9XG6Xwj3I-ystLR-DEQ5OetMORXBmcTueKmcs,1952
|
|
6
|
+
aiogram_cmds/customize.py,sha256=pLTzVw2W8vng5WN9CeTeMXHGbp8R9z_HJfYEz6zNfc4,1934
|
|
7
|
+
aiogram_cmds/dynamic.py,sha256=mGA05euNAbdc4rN-u8ROgUzaMk9U0-DLSDnUOLQAItc,1062
|
|
8
|
+
aiogram_cmds/manager.py,sha256=fuF_1wR_0C--b5v1rl0dRmh0IiPmNcuEMgNjSb8nTLY,4990
|
|
9
|
+
aiogram_cmds/policy.py,sha256=Ex68-vtYonUqbdC1p00GXSPSToHW5hTLF3s01snr0c0,990
|
|
10
|
+
aiogram_cmds/registry.py,sha256=5-tG-Cz3pAg4iNHiKuOG2RM3C-kXi1hzO7k6j0istNc,646
|
|
11
|
+
aiogram_cmds/setters.py,sha256=dKkM0VIoug2-LJfzTnUgpdB-YXD720hivXn3ssswnuI,2748
|
|
12
|
+
aiogram_cmds/settings.py,sha256=UctpHLsbP_jthkX1_zIKB_2ZSygz9W9v0n8WDiHWHTs,1656
|
|
13
|
+
aiogram_cmds/translator.py,sha256=499Y3nmu78vj-TZcaEWpN59ycV4osjHOgELsvOUzMuY,818
|
|
14
|
+
aiogram_cmds/version.py,sha256=NdAm1Oirm_zG5tKFJB3vRPaOpBT057H5usna3_9MM-g,67
|
|
15
|
+
aiogram_cmds-0.1.0.dist-info/METADATA,sha256=JZHTu58X1p53jcD8f31w7LIq4G2ORCvwOAeOPwldLRk,10002
|
|
16
|
+
aiogram_cmds-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
17
|
+
aiogram_cmds-0.1.0.dist-info/licenses/LICENSE,sha256=uboqwLY50frOnHJ4Mo4x3F85t7WiVK5CJRNBNokhSOg,1085
|
|
18
|
+
aiogram_cmds-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 aiogram-sentinel contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|