Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion handlers/bots/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
37 changes: 32 additions & 5 deletions handlers/bots/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
Comment thread
gordonkoehn marked this conversation as resolved.
)
keyboard.append(row)
Expand Down Expand Up @@ -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)

Expand Down