Skip to content

Commit bf52f73

Browse files
dsammarugabeanrepo
authored andcommitted
Add auto-register commands feature
1 parent 1f31099 commit bf52f73

File tree

1 file changed

+54
-3
lines changed

1 file changed

+54
-3
lines changed

src/arduino/app_bricks/telegram_bot/telegram_bot.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import time
1010
from typing import Callable, Optional
1111
from arduino.app_utils import brick, Logger
12-
from telegram import Update
12+
from telegram import Update, BotCommand
1313
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
1414
from 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

Comments
 (0)