diff --git a/handlers/bots/__init__.py b/handlers/bots/__init__.py index 3ef6e6b3..75e4d71f 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 + # 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: + idx = int(action_parts[1]) + except ValueError: + logger.warning( + f"Non-integer bot_idx callback: {action_parts[1]!r}" + ) + return + 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: + logger.warning( + f"bot_idx {idx} out of range (cached list has " + f"{len(bots_list)} entries); re-opening 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..4c59d27b 100644 --- a/handlers/bots/menu.py +++ b/handlers/bots/menu.py @@ -50,29 +50,47 @@ 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 + # + # 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 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=_bot_callback(i, bot_name), ) ] ) 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=_bot_callback(j, bot_name), ) ) keyboard.append(row) @@ -171,6 +189,15 @@ 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. 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)