From b81400105fd2d06affbbc23be003918d7ef30508 Mon Sep 17 00:00:00 2001 From: PEDZEO Date: Fri, 19 Dec 2025 04:02:58 +0300 Subject: [PATCH] Add button open mode and webapp url --- app/services/menu_layout_service.py | 16 ++++++++++++++-- app/webapi/routes/menu_layout.py | 11 +++++++++++ app/webapi/schemas/menu_layout.py | 24 ++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/app/services/menu_layout_service.py b/app/services/menu_layout_service.py index cd4b9764..50886b8c 100644 --- a/app/services/menu_layout_service.py +++ b/app/services/menu_layout_service.py @@ -135,6 +135,8 @@ DEFAULT_MENU_CONFIG: Dict[str, Any] = { "visibility": "subscribers", "conditions": None, "dynamic_text": False, + "open_mode": "callback", # "callback" или "direct" + "webapp_url": None, # URL для Mini App при open_mode="direct" }, "happ_download": { "type": "builtin", @@ -308,6 +310,7 @@ BUILTIN_BUTTONS_INFO = [ "callback_data": "subscription_connect", "default_conditions": {"has_active_subscription": True, "subscription_is_active": True}, "supports_dynamic_text": False, + "supports_direct_open": True, # Может открывать Mini App напрямую }, { "id": "happ_download", @@ -1021,6 +1024,8 @@ class MenuLayoutService: button_type = button_config.get("type", "builtin") text_config = button_config.get("text", {}) action = button_config.get("action", "") + open_mode = button_config.get("open_mode", "callback") + webapp_url = button_config.get("webapp_url") # Получаем текст text = cls._get_localized_text(text_config, context.language) @@ -1039,8 +1044,15 @@ class MenuLayoutService: text=text, web_app=types.WebAppInfo(url=action) ) else: - # builtin - callback_data - return InlineKeyboardButton(text=text, callback_data=action) + # builtin - проверяем open_mode + if open_mode == "direct" and webapp_url: + # Прямое открытие Mini App через WebAppInfo + return InlineKeyboardButton( + text=text, web_app=types.WebAppInfo(url=webapp_url) + ) + else: + # Стандартный callback_data + return InlineKeyboardButton(text=text, callback_data=action) @classmethod async def build_keyboard( diff --git a/app/webapi/routes/menu_layout.py b/app/webapi/routes/menu_layout.py index cd87e5a5..8eb7a6aa 100644 --- a/app/webapi/routes/menu_layout.py +++ b/app/webapi/routes/menu_layout.py @@ -71,6 +71,8 @@ def _serialize_config(config: dict, is_enabled: bool, updated_at) -> MenuLayoutR if btn_data.get("conditions") else None, dynamic_text=btn_data.get("dynamic_text", False), + open_mode=btn_data.get("open_mode", "callback"), + webapp_url=btn_data.get("webapp_url"), ) return MenuLayoutResponse( @@ -143,6 +145,7 @@ async def list_builtin_buttons( if btn_info.get("default_conditions") else None, supports_dynamic_text=btn_info.get("supports_dynamic_text", False), + supports_direct_open=btn_info.get("supports_direct_open", False), ) ) @@ -163,6 +166,10 @@ async def update_button( if "visibility" in updates and updates["visibility"] is not None: if hasattr(updates["visibility"], "value"): updates["visibility"] = updates["visibility"].value + # Конвертируем open_mode в строку если есть + if "open_mode" in updates and updates["open_mode"] is not None: + if hasattr(updates["open_mode"], "value"): + updates["open_mode"] = updates["open_mode"].value # Конвертируем conditions - убираем None значения если это dict if "conditions" in updates and updates["conditions"] is not None: if isinstance(updates["conditions"], dict): @@ -183,6 +190,8 @@ async def update_button( if button.get("conditions") else None, dynamic_text=button.get("dynamic_text", False), + open_mode=button.get("open_mode", "callback"), + webapp_url=button.get("webapp_url"), ) except KeyError as e: raise HTTPException(status.HTTP_404_NOT_FOUND, str(e)) from e @@ -290,6 +299,8 @@ async def add_custom_button( if button.get("conditions") else None, dynamic_text=button.get("dynamic_text", False), + open_mode=button.get("open_mode", "callback"), + webapp_url=button.get("webapp_url"), ) except ValueError as e: raise HTTPException(status.HTTP_400_BAD_REQUEST, str(e)) from e diff --git a/app/webapi/schemas/menu_layout.py b/app/webapi/schemas/menu_layout.py index 100aacaa..bb413534 100644 --- a/app/webapi/schemas/menu_layout.py +++ b/app/webapi/schemas/menu_layout.py @@ -26,6 +26,13 @@ class ButtonVisibility(str, Enum): SUBSCRIBERS = "subscribers" # Только подписчикам +class ButtonOpenMode(str, Enum): + """Режим открытия кнопки.""" + + CALLBACK = "callback" # Отправляет callback_data боту (по умолчанию) + DIRECT = "direct" # Сразу открывает Mini App через WebAppInfo + + class ButtonConditions(BaseModel): """Условия показа кнопки.""" @@ -96,6 +103,14 @@ class MenuButtonConfig(BaseModel): dynamic_text: bool = Field( default=False, description="Текст содержит плейсхолдеры ({balance} и т.д.)" ) + open_mode: ButtonOpenMode = Field( + default=ButtonOpenMode.CALLBACK, + description="Режим открытия: callback (через бота) или direct (сразу Mini App)", + ) + webapp_url: Optional[str] = Field( + default=None, + description="URL для Mini App при open_mode=direct", + ) model_config = ConfigDict(extra="forbid") @@ -156,6 +171,9 @@ class BuiltinButtonInfo(BaseModel): supports_dynamic_text: bool = Field( default=False, description="Поддерживает ли динамический текст" ) + supports_direct_open: bool = Field( + default=False, description="Поддерживает ли прямое открытие Mini App" + ) class BuiltinButtonsListResponse(BaseModel): @@ -197,6 +215,12 @@ class ButtonUpdateRequest(BaseModel): action: Optional[str] = Field( default=None, description="Новый action (для URL/MiniApp кнопок)" ) + open_mode: Optional[ButtonOpenMode] = Field( + default=None, description="Режим открытия: callback или direct" + ) + webapp_url: Optional[str] = Field( + default=None, description="URL для Mini App при open_mode=direct" + ) model_config = ConfigDict(extra="forbid")