From f162f19e3c9fb9caef2f61004424914ac7e71e96 Mon Sep 17 00:00:00 2001 From: gordon J Koehn Date: Sun, 19 Apr 2026 10:27:05 +0200 Subject: [PATCH 1/2] fix(bots): avoid Telegram 64-byte callback_data overflow on bots menu Long bot_names (e.g. hummingbot-deploy's 'pmm-EPIC-USDT-kucoin-v1--' format, 54 chars) plus the 'bots:bot_detail:' prefix (16 bytes) exceed Telegram's 64-byte InlineKeyboardButton.callback_data cap, producing telegram.error.BadRequest: Button_data_invalid and rendering /bots unusable. Switch the main bots menu to index-based callback_data (bots:bot_idx:{i}), stashing the ordered bot-name list in context.chat_data['bots_main_list'] when the menu is built. A new dispatcher branch resolves the index back to the name and calls show_bot_detail(). Legacy bots:bot_detail:{name} branch retained for callers that pass known-short names. Pattern mirrors the existing ctrl_idx mechanism in the same dispatcher. --- handlers/bots/__init__.py | 27 ++++++++++++++++++++++++++- handlers/bots/menu.py | 23 ++++++++++++++++++----- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/handlers/bots/__init__.py b/handlers/bots/__init__.py index 3ef6e6b3..7f211da5 100644 --- a/handlers/bots/__init__.py +++ b/handlers/bots/__init__.py @@ -674,12 +674,37 @@ async def bots_callback_handler( setting = action_parts[1] await handle_pmm_adv_setting(update, context, setting) - # Bot detail + # Bot detail (legacy: bot_name embedded directly — retained for any + # callers where the name length is known-safe). elif main_action == "bot_detail": if len(action_parts) > 1: bot_name = action_parts[1] await show_bot_detail(update, context, bot_name) + # Bot detail by index — resolves the index to a bot_name from the + # cached list stashed by show_bots_menu. Keeps callback_data under + # Telegram's 64-byte cap regardless of bot_name length. + elif main_action == "bot_idx": + if len(action_parts) > 1: + try: + idx = int(action_parts[1]) + except ValueError: + logger.warning( + f"Non-integer bot_idx callback: {action_parts[1]!r}" + ) + return + bots_list = context.chat_data.get("bots_main_list", []) + if 0 <= idx < len(bots_list): + await show_bot_detail(update, context, bots_list[idx]) + else: + logger.warning( + f"bot_idx {idx} out of range (cached list has " + f"{len(bots_list)} entries); re-opening bots menu" + ) + from handlers.bots.menu import show_bots_menu + + await show_bots_menu(update, context) + # Controller detail (by index, uses context) elif main_action == "ctrl_idx": if len(action_parts) > 1: diff --git a/handlers/bots/menu.py b/handlers/bots/menu.py index bdefebb9..f4a1f063 100644 --- a/handlers/bots/menu.py +++ b/handlers/bots/menu.py @@ -50,29 +50,36 @@ def _build_main_menu_keyboard(bots_dict: Dict[str, Any]) -> InlineKeyboardMarkup # For 1-3 bots: one per row # For 4+ bots: two per row for better space utilization + # + # Use index-based callback_data (bots:bot_idx:{i}) instead of embedding + # bot_name directly. Telegram's InlineKeyboardButton.callback_data cap is + # 64 bytes; long bot_names (e.g. "pmm-EPIC-USDT-kucoin-v1--") plus + # the "bots:bot_detail:" prefix (16 bytes) routinely overflow and trigger + # telegram.error.BadRequest: Button_data_invalid. The dispatcher resolves + # the index back to the bot_name from context.chat_data["bots_main_list"]. if len(bot_names) <= 3: - for bot_name in bot_names: + for i, bot_name in enumerate(bot_names): display_name = bot_name[:30] + "..." if len(bot_name) > 30 else bot_name keyboard.append( [ InlineKeyboardButton( f"📊 {display_name}", - callback_data=f"bots:bot_detail:{bot_name}", + callback_data=f"bots:bot_idx:{i}", ) ] ) else: # Two bots per row for better space utilization - for i in range(0, len(bot_names), 2): + for row_start in range(0, len(bot_names), 2): row = [] - for j in range(i, min(i + 2, len(bot_names))): + for j in range(row_start, min(row_start + 2, len(bot_names))): bot_name = bot_names[j] # Shorter display name when two per row display_name = bot_name[:25] + "..." if len(bot_name) > 25 else bot_name row.append( InlineKeyboardButton( f"📊 {display_name}", - callback_data=f"bots:bot_detail:{bot_name}", + callback_data=f"bots:bot_idx:{j}", ) ) keyboard.append(row) @@ -171,6 +178,12 @@ async def show_bots_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> # Format the bot status message status_message = format_active_bots(bots_data, bot_runs=bot_runs_map) + # Cache the ordered bot-name list so the dispatcher can resolve the + # index-based callback (bots:bot_idx:{i}) back to a name. Must be set + # BEFORE building the keyboard so the indices we stash match the + # indices embedded in the buttons. + context.chat_data["bots_main_list"] = list(bots_dict.keys()) + # Build the menu with bot buttons reply_markup = _build_main_menu_keyboard(bots_dict) From 384c7f2a1e31ebe3c0796df72b578a7b7cac9ff0 Mon Sep 17 00:00:00 2001 From: gordon J Koehn Date: Sun, 19 Apr 2026 15:54:01 +0200 Subject: [PATCH 2/2] address review: user_data + hybrid name/idx callback + drop redundant import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses three Copilot review comments on hummingbot/condor#61: 1. Use context.user_data (not chat_data) for the cached bot-name list so group-chat users don't overwrite each other's state. Matches the per-user convention used elsewhere in this handler. 2. Prefer the stable name-based callback_data form when it fits the 64-byte cap, fall back to index-based only when the name would overflow. Short-named bots keep the original upstream behavior: buttons on stale menu messages stay correct across list reordering. Only long-named bots (>= 49 chars) use the index-based form, where list stability is a hard trade-off vs not crashing. 3. Drop the redundant local re-import of show_bots_menu inside the bot_idx dispatcher branch — already imported at module scope. Validated end-to-end: same 72-char bot name exercises the index path cleanly. --- handlers/bots/__init__.py | 10 +++++----- handlers/bots/menu.py | 34 ++++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/handlers/bots/__init__.py b/handlers/bots/__init__.py index 7f211da5..75e4d71f 100644 --- a/handlers/bots/__init__.py +++ b/handlers/bots/__init__.py @@ -682,8 +682,10 @@ async def bots_callback_handler( await show_bot_detail(update, context, bot_name) # Bot detail by index — resolves the index to a bot_name from the - # cached list stashed by show_bots_menu. Keeps callback_data under - # Telegram's 64-byte cap regardless of bot_name length. + # per-user cache stashed by show_bots_menu. Keeps callback_data under + # Telegram's 64-byte cap for long bot_names that would otherwise + # overflow the name-based callback form. Cache lives in user_data (not + # chat_data) so group-chat users don't clobber each other's state. elif main_action == "bot_idx": if len(action_parts) > 1: try: @@ -693,7 +695,7 @@ async def bots_callback_handler( f"Non-integer bot_idx callback: {action_parts[1]!r}" ) return - bots_list = context.chat_data.get("bots_main_list", []) + bots_list = context.user_data.get("bots_main_list", []) if 0 <= idx < len(bots_list): await show_bot_detail(update, context, bots_list[idx]) else: @@ -701,8 +703,6 @@ async def bots_callback_handler( f"bot_idx {idx} out of range (cached list has " f"{len(bots_list)} entries); re-opening bots menu" ) - from handlers.bots.menu import show_bots_menu - await show_bots_menu(update, context) # Controller detail (by index, uses context) diff --git a/handlers/bots/menu.py b/handlers/bots/menu.py index f4a1f063..4c59d27b 100644 --- a/handlers/bots/menu.py +++ b/handlers/bots/menu.py @@ -51,12 +51,23 @@ def _build_main_menu_keyboard(bots_dict: Dict[str, Any]) -> InlineKeyboardMarkup # For 1-3 bots: one per row # For 4+ bots: two per row for better space utilization # - # Use index-based callback_data (bots:bot_idx:{i}) instead of embedding - # bot_name directly. Telegram's InlineKeyboardButton.callback_data cap is - # 64 bytes; long bot_names (e.g. "pmm-EPIC-USDT-kucoin-v1--") plus - # the "bots:bot_detail:" prefix (16 bytes) routinely overflow and trigger - # telegram.error.BadRequest: Button_data_invalid. The dispatcher resolves - # the index back to the bot_name from context.chat_data["bots_main_list"]. + # callback_data strategy: Telegram caps InlineKeyboardButton.callback_data + # at 64 bytes. The original `bots:bot_detail:{bot_name}` form (16-byte + # prefix + name) overflows for any bot_name >= 49 chars — e.g. names + # produced by hummingbot-api's deploy-v2-controllers endpoint, which + # auto-appends a deploy-time timestamp, routinely run past 70 bytes and + # crash /bots with telegram.error.BadRequest: Button_data_invalid. + # + # Strategy: prefer the stable name-based form when it fits (preserves the + # original behavior — stale buttons from an earlier /bots message still + # open the correct bot regardless of list reordering). Fall back to the + # index-based form only when the name-based form would overflow. The + # dispatcher resolves the index against a per-user cache stashed in + # context.user_data["bots_main_list"] by show_bots_menu. + def _bot_callback(i: int, name: str) -> str: + cb = f"bots:bot_detail:{name}" + return cb if len(cb.encode("utf-8")) <= 64 else f"bots:bot_idx:{i}" + if len(bot_names) <= 3: for i, bot_name in enumerate(bot_names): display_name = bot_name[:30] + "..." if len(bot_name) > 30 else bot_name @@ -64,7 +75,7 @@ def _build_main_menu_keyboard(bots_dict: Dict[str, Any]) -> InlineKeyboardMarkup [ InlineKeyboardButton( f"📊 {display_name}", - callback_data=f"bots:bot_idx:{i}", + callback_data=_bot_callback(i, bot_name), ) ] ) @@ -79,7 +90,7 @@ def _build_main_menu_keyboard(bots_dict: Dict[str, Any]) -> InlineKeyboardMarkup row.append( InlineKeyboardButton( f"📊 {display_name}", - callback_data=f"bots:bot_idx:{j}", + callback_data=_bot_callback(j, bot_name), ) ) keyboard.append(row) @@ -181,8 +192,11 @@ async def show_bots_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> # Cache the ordered bot-name list so the dispatcher can resolve the # index-based callback (bots:bot_idx:{i}) back to a name. Must be set # BEFORE building the keyboard so the indices we stash match the - # indices embedded in the buttons. - context.chat_data["bots_main_list"] = list(bots_dict.keys()) + # indices embedded in the buttons. Stored in user_data (not chat_data) + # so two users sharing a group chat don't overwrite each other's + # cached list — matches the per-user convention used elsewhere in + # this handler (e.g. current_controllers in __init__.py). + context.user_data["bots_main_list"] = list(bots_dict.keys()) # Build the menu with bot buttons reply_markup = _build_main_menu_keyboard(bots_dict)