diff --git a/handlers/bots/__init__.py b/handlers/bots/__init__.py index 3ef6e6b3..f3da8de0 100644 --- a/handlers/bots/__init__.py +++ b/handlers/bots/__init__.py @@ -173,6 +173,21 @@ logger = logging.getLogger(__name__) +def _split_action_payload(action: str) -> tuple[str, str | None]: + main_action, separator, payload = action.partition(":") + return main_action, payload if separator else None + + +_PAIR_PAYLOAD_HANDLERS = { + "pv1_pair": handle_pv1_wizard_pair, + "pv1_pair_select": handle_pv1_pair_select, + "gs_pair": handle_gs_wizard_pair, + "gs_pair_select": handle_gs_pair_select, + "pmm_pair": handle_pmm_wizard_pair, + "pmm_pair_select": handle_pmm_pair_select, +} + + # ============================================ # MAIN BOTS COMMAND # ============================================ @@ -258,8 +273,15 @@ async def bots_callback_handler( action = callback_parts[1] if len(callback_parts) > 1 else query.data # Parse action and any additional parameters + main_action, action_payload = _split_action_payload(action) action_parts = action.split(":") - main_action = action_parts[0] + + if main_action in _PAIR_PAYLOAD_HANDLERS: + if action_payload is not None: + await _PAIR_PAYLOAD_HANDLERS[main_action]( + update, context, action_payload + ) + return # Menu navigation if main_action == "main_menu": @@ -288,18 +310,16 @@ async def bots_callback_handler( await show_type_selector(update, context) elif main_action == "cfg_type": - if len(action_parts) > 1: - controller_type = action_parts[1] - await show_configs_by_type(update, context, controller_type) + if action_payload is not None: + await show_configs_by_type(update, context, action_payload) elif main_action == "cfg_toggle": - if len(action_parts) > 1: - config_id = action_parts[1] - await handle_cfg_toggle(update, context, config_id) + if action_payload is not None: + await handle_cfg_toggle(update, context, action_payload) elif main_action == "cfg_page": - if len(action_parts) > 1: - page = int(action_parts[1]) + if action_payload is not None: + page = int(action_payload) await handle_cfg_page(update, context, page) elif main_action == "cfg_clear_selection": @@ -368,16 +388,6 @@ async def bots_callback_handler( connector = action_parts[1] await handle_pv1_wizard_connector(update, context, connector) - elif main_action == "pv1_pair": - if len(action_parts) > 1: - pair = action_parts[1] - await handle_pv1_wizard_pair(update, context, pair) - - elif main_action == "pv1_pair_select": - if len(action_parts) > 1: - pair = action_parts[1] - await handle_pv1_pair_select(update, context, pair) - elif main_action == "pv1_amount": if len(action_parts) > 1: amount = action_parts[1] @@ -487,10 +497,8 @@ async def bots_callback_handler( await handle_select_credentials(update, context, creds) elif main_action == "select_image": - if len(action_parts) > 1: - # Rejoin parts to preserve colons in image tag (e.g., "hummingbot:development") - image = ":".join(action_parts[1:]) - await handle_select_image(update, context, image) + if action_payload is not None: + await handle_select_image(update, context, action_payload) elif main_action == "select_name": if len(action_parts) > 1: @@ -509,16 +517,6 @@ async def bots_callback_handler( connector = action_parts[1] await handle_gs_wizard_connector(update, context, connector) - elif main_action == "gs_pair": - if len(action_parts) > 1: - pair = action_parts[1] - await handle_gs_wizard_pair(update, context, pair) - - elif main_action == "gs_pair_select": - if len(action_parts) > 1: - pair = action_parts[1] - await handle_gs_pair_select(update, context, pair) - elif main_action == "gs_side": if len(action_parts) > 1: side_str = action_parts[1] @@ -606,16 +604,6 @@ async def bots_callback_handler( connector = action_parts[1] await handle_pmm_wizard_connector(update, context, connector) - elif main_action == "pmm_pair": - if len(action_parts) > 1: - pair = action_parts[1] - await handle_pmm_wizard_pair(update, context, pair) - - elif main_action == "pmm_pair_select": - if len(action_parts) > 1: - pair = action_parts[1] - await handle_pmm_pair_select(update, context, pair) - elif main_action == "pmm_leverage": if len(action_parts) > 1: leverage = int(action_parts[1]) diff --git a/handlers/bots/controller_handlers.py b/handlers/bots/controller_handlers.py index edfe0e88..1656e247 100644 --- a/handlers/bots/controller_handlers.py +++ b/handlers/bots/controller_handlers.py @@ -14,6 +14,7 @@ import asyncio import copy +import hashlib import logging from typing import List @@ -74,6 +75,26 @@ CONFIGS_PER_PAGE = 8 # Reduced to leave space for action buttons +def _config_callback_token(config_id: str) -> str: + """Short stable token for Telegram callback_data's 64-byte limit.""" + return hashlib.blake2s(config_id.encode("utf-8"), digest_size=6).hexdigest() + + +def _resolve_config_callback_token(context: ContextTypes.DEFAULT_TYPE, token: str) -> str: + """Resolve a short callback token to the original config ID.""" + callback_ids = context.user_data.get("config_callback_ids", {}) + return callback_ids.get(token, token) + + +def _escape_markdown_v2_code(text: object) -> str: + """Escape text for a MarkdownV2 inline code span.""" + return str(text).replace("\\", "\\\\").replace("`", "\\`").replace("\n", "\\n") + + +def _code_span(text: object) -> str: + return f"`{_escape_markdown_v2_code(text)}`" + + def _get_controller_type_display(controller_name: str) -> tuple[str, str]: """Get display name and emoji for controller type""" type_map = { @@ -163,6 +184,11 @@ async def show_controller_configs_menu( # Store all configs context.user_data["controller_configs_list"] = configs + context.user_data["config_callback_ids"] = { + _config_callback_token(cfg.get("id", "")): cfg.get("id", "") + for cfg in configs + if cfg.get("id") + } # Get available types from registry (always shows all supported types) all_types = get_supported_controller_types() @@ -258,6 +284,7 @@ async def show_controller_configs_menu( # Config checkboxes - show just the controller name/ID for i, cfg in enumerate(page_configs): config_id = cfg.get("id", f"config_{start_idx + i}") + config_token = _config_callback_token(config_id) is_selected = selected.get(config_id, False) checkbox = "✅" if is_selected else "⬜" @@ -267,7 +294,7 @@ async def show_controller_configs_menu( keyboard.append( [ InlineKeyboardButton( - display, callback_data=f"bots:cfg_toggle:{config_id}" + display, callback_data=f"bots:cfg_toggle:{config_token}" ) ] ) @@ -453,6 +480,7 @@ async def handle_cfg_toggle( update: Update, context: ContextTypes.DEFAULT_TYPE, config_id: str ) -> None: """Toggle config selection by config ID""" + config_id = _resolve_config_callback_token(context, config_id) selected = context.user_data.get("selected_configs", {}) if selected.get(config_id): @@ -734,7 +762,7 @@ async def show_cfg_edit_form( if status_msg: header += f" — {escape_markdown_v2(status_msg)}" lines = [header, ""] - lines.append(f"`{escape_markdown_v2(config_id)}`") + lines.append(_code_span(config_id)) lines.append("") # Add context info for Grid Strike (connector, trading pair, side) @@ -751,9 +779,9 @@ async def show_cfg_edit_form( # Build config text for display (each line copyable) for key, value in editable_fields.items(): - lines.append(f"`{key}={value}`") + lines.append(_code_span(f"{key}={value}")) lines.append("") - lines.append("✏️ _Send `key=value` to update_") + lines.append(f"✏️ Send {_code_span('key=value')} to update") # Build keyboard - simplified, no field buttons keyboard = [] @@ -994,7 +1022,7 @@ async def process_cfg_edit_input( config_id = config.get("id", "unknown") lines = [f"*Edit Config* \\({current_idx + 1}/{total}\\)", ""] - lines.append(f"`{escape_markdown_v2(config_id)}`") + lines.append(_code_span(config_id)) lines.append("") # Add context info for Grid Strike (connector, trading pair, side) @@ -1010,9 +1038,9 @@ async def process_cfg_edit_input( lines.append("") for key, value in editable_fields.items(): - lines.append(f"`{key}={value}`") + lines.append(_code_span(f"{key}={value}")) lines.append("") - lines.append("✏️ _Send `key=value` to update_") + lines.append(f"✏️ Send {_code_span('key=value')} to update") # Build keyboard keyboard = [] diff --git a/handlers/cex/_shared.py b/handlers/cex/_shared.py index 521211e9..99a6bbb9 100644 --- a/handlers/cex/_shared.py +++ b/handlers/cex/_shared.py @@ -671,8 +671,8 @@ async def validate_trading_pair( # Log a few sample pairs to debug sample_pairs = available_pairs[:5] if available_pairs else [] logger.info(f"Sample pairs: {sample_pairs}") - - # For input like "XYZ", look for pairs like "ISSUER:XYZ-USDC" or "ISSUER:XYZ-USD" + + # For input like "XYZ" or "ISSUER:XYZ", look for pairs like "ISSUER:XYZ-USDC" or "ISSUER:XYZ-USD". hip3_matches = [] for pair in available_pairs: if ":" in pair: @@ -684,16 +684,21 @@ async def validate_trading_pair( if ":" in issuer_symbol: issuer, symbol = issuer_symbol.split(":", 1) logger.debug(f"Checking pair '{pair}': issuer='{issuer}', symbol='{symbol}', quote='{quote}' vs input='{pair_normalized}'") - # Check if the input matches the symbol part - if symbol.upper() == pair_normalized.upper(): - logger.info(f"Found HIP3 symbol match: '{pair}' matches input '{pair_normalized}'") - hip3_matches.append(pair) - # Also check if input matches the full issuer:symbol part - elif ( - issuer_symbol.upper().replace(":", "-") - == pair_normalized.upper() - ): - logger.info(f"Found HIP3 issuer:symbol match: '{pair}' matches input '{pair_normalized}'") + + issuer_symbol_normalized = issuer_symbol.upper().replace( + "_", "-" + ).replace("/", "-") + hip3_aliases = { + symbol.upper(), + f"{symbol.upper()}-{quote.upper()}", + issuer_symbol_normalized, + issuer_symbol_normalized.replace(":", "-"), + pair.upper().replace("_", "-").replace("/", "-"), + } + if pair_normalized.upper() in hip3_aliases: + logger.info( + f"Found HIP3 match: '{pair}' matches input '{pair_normalized}'" + ) hip3_matches.append(pair) except ValueError as e: logger.debug(f"Failed to parse HIP3 pair '{pair}': {e}")