99import time
1010from typing import Callable , Optional
1111from arduino .app_utils import brick , Logger
12- from telegram import Update
12+ from telegram import Update , BotCommand
1313from telegram .ext import Application , CommandHandler , MessageHandler , filters , ContextTypes
1414from telegram .error import NetworkError , TimedOut
1515
@@ -32,6 +32,7 @@ def __init__(
3232 message_timeout : int = 30 ,
3333 photo_timeout : int = 60 ,
3434 max_retries : int = 3 ,
35+ auto_set_commands : bool = True ,
3536 ) -> None :
3637 """Initialize the Telegram bot with configurable timeouts and retry settings.
3738
@@ -41,6 +42,9 @@ def __init__(
4142 message_timeout: Timeout in seconds for sending messages (default: 30).
4243 photo_timeout: Timeout in seconds for sending/downloading photos (default: 60).
4344 max_retries: Maximum number of retries for network operations (default: 3).
45+ auto_set_commands: Automatically sync registered commands with Telegram's
46+ command menu (default: True). When enabled, commands with descriptions
47+ will appear when users type '/' in the chat.
4448
4549 Raises:
4650 ValueError: If token is not provided and TELEGRAM_BOT_TOKEN env var is not set.
@@ -52,13 +56,15 @@ def __init__(
5256 self .message_timeout = message_timeout
5357 self .photo_timeout = photo_timeout
5458 self .max_retries = max_retries
59+ self .auto_set_commands = auto_set_commands
5560
5661 self .application = Application .builder ().token (self .token ).build ()
5762 self ._loop : Optional [asyncio .AbstractEventLoop ] = None
5863 self ._loop_thread : Optional [threading .Thread ] = None
5964 self ._running : bool = False
6065 self ._initialized : bool = False
6166 self ._scheduled_tasks : dict [str , threading .Timer ] = {}
67+ self ._commands_registry : dict [str , str ] = {}
6268
6369 def _make_async_handler (self , callback : Callable ) -> Callable :
6470 """Convert a synchronous callback to an async handler if needed.
@@ -80,15 +86,33 @@ async def async_wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
8086
8187 return async_wrapper
8288
83- def add_command (self , command : str , callback : Callable ) -> None :
84- """Register a slash command handler.
89+ def add_command (self , command : str , callback : Callable , description : str = "" ) -> None :
90+ """Register a slash command handler with optional description.
91+
92+ The callback can be either synchronous or asynchronous. If synchronous,
93+ it will be automatically converted to async. If a description is provided
94+ and auto_set_commands is enabled, the command will appear in Telegram's
95+ command menu when users type '/'.
8596
8697 Args:
8798 command: Command name (without the leading slash, e.g., "start" for /start).
8899 callback: Handler function (can be sync or async). Receives Update and ContextTypes.
100+ description: Optional description shown in Telegram's command menu. If empty,
101+ the command will still work but won't appear in the '/' menu.
102+
103+ Example:
104+ >>> bot.add_command("start", start_handler, "Start the bot")
105+ >>> bot.add_command("help", help_handler, "Show available commands")
89106 """
90107 async_callback = self ._make_async_handler (callback )
91108 self .application .add_handler (CommandHandler (command , async_callback ))
109+
110+ # Track command for auto-sync with Telegram
111+ if description :
112+ self ._commands_registry [command ] = description
113+ logger .info (f"Registered command /{ command } : { description } " )
114+ else :
115+ logger .info (f"Registered command /{ command } " )
92116
93117 def on_text (self , callback : Callable ) -> None :
94118 """Register a handler for text messages.
@@ -289,6 +313,29 @@ async def _get_photo_async(self, update: Update, context: ContextTypes.DEFAULT_T
289313 logger .error (f"An error occurred while downloading photo: { e } " )
290314 raise
291315
316+ async def _set_bot_commands (self ) -> None :
317+ """Internal method to sync registered commands with Telegram.
318+
319+ This updates the bot's command menu that appears when users type '/'.
320+ Only commands with descriptions are registered.
321+
322+ Raises:
323+ Exception: If setting commands fails (logged but not raised).
324+ """
325+ if not self ._commands_registry :
326+ logger .info ("No commands with descriptions to register with Telegram" )
327+ return
328+
329+ try :
330+ bot_commands = [
331+ BotCommand (command = cmd , description = desc )
332+ for cmd , desc in self ._commands_registry .items ()
333+ ]
334+ await self .application .bot .set_my_commands (bot_commands )
335+ logger .info (f"Successfully registered { len (bot_commands )} command(s) with Telegram's menu" )
336+ except Exception as e :
337+ logger .error (f"Failed to set bot commands: { e } " )
338+
292339 def schedule_message (
293340 self ,
294341 chat_id : int ,
@@ -445,6 +492,10 @@ def _run_bot(self) -> None:
445492 self ._loop .run_until_complete (self .application .start ())
446493 self ._loop .run_until_complete (self .application .updater .start_polling (allowed_updates = Update .ALL_TYPES ))
447494
495+ # Auto-register commands with Telegram after polling starts
496+ if self .auto_set_commands :
497+ self ._loop .run_until_complete (self ._set_bot_commands ())
498+
448499 self ._initialized = True # Signal successful initialization
449500 logger .info ("Bot polling started successfully" )
450501
0 commit comments