mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-19 19:32:10 +00:00
Enhance button handling in MenuLayoutService to improve connect button identification and URL management
This commit is contained in:
@@ -270,10 +270,44 @@ class MenuLayoutService:
|
||||
config = config.copy()
|
||||
buttons = config.get("buttons", {})
|
||||
|
||||
# Улучшенное определение кнопки connect для разных форматов ID
|
||||
actual_button_id = button_id
|
||||
if button_id not in buttons:
|
||||
raise KeyError(f"Button '{button_id}' not found")
|
||||
# Пробуем найти кнопку connect по разным форматам
|
||||
if "connect" in button_id.lower():
|
||||
# Проверяем разные варианты: connect, callback:connect и т.д.
|
||||
for key in buttons.keys():
|
||||
if key == "connect" or buttons[key].get("builtin_id") == "connect":
|
||||
actual_button_id = key
|
||||
logger.info(
|
||||
f"🔗 Найдена кнопка connect по ID '{button_id}' -> '{actual_button_id}'"
|
||||
)
|
||||
break
|
||||
else:
|
||||
# Если не нашли, пробуем найти по builtin_id
|
||||
for key, button in buttons.items():
|
||||
if button.get("builtin_id") == "connect" or "connect" in str(button.get("builtin_id", "")).lower():
|
||||
actual_button_id = key
|
||||
logger.info(
|
||||
f"🔗 Найдена кнопка connect по builtin_id '{button_id}' -> '{actual_button_id}'"
|
||||
)
|
||||
break
|
||||
else:
|
||||
raise KeyError(f"Button '{button_id}' not found")
|
||||
|
||||
button = buttons[button_id].copy()
|
||||
if actual_button_id not in buttons:
|
||||
raise KeyError(f"Button '{actual_button_id}' not found")
|
||||
|
||||
button = buttons[actual_button_id].copy()
|
||||
|
||||
# Логирование для отладки
|
||||
if "connect" in actual_button_id.lower() or button.get("builtin_id") == "connect":
|
||||
logger.info(
|
||||
f"🔗 Обновление кнопки connect (ID: {actual_button_id}): "
|
||||
f"open_mode={updates.get('open_mode')}, "
|
||||
f"action={updates.get('action')}, "
|
||||
f"webapp_url={updates.get('webapp_url')}"
|
||||
)
|
||||
|
||||
# Применяем обновления
|
||||
if "text" in updates and updates["text"] is not None:
|
||||
@@ -296,12 +330,16 @@ class MenuLayoutService:
|
||||
# Для URL/MiniApp/callback кнопок можно менять action
|
||||
if button.get("type") in ("url", "mini_app", "callback"):
|
||||
button["action"] = updates["action"]
|
||||
# Для builtin кнопок можно менять action, если open_mode == "direct"
|
||||
# Это позволяет указать URL Mini App в поле action для кнопки connect
|
||||
elif button.get("type") == "builtin" and updates.get("open_mode") == "direct":
|
||||
button["action"] = updates["action"]
|
||||
if "open_mode" in updates and updates["open_mode"] is not None:
|
||||
button["open_mode"] = updates["open_mode"]
|
||||
if "webapp_url" in updates:
|
||||
button["webapp_url"] = updates["webapp_url"]
|
||||
|
||||
buttons[button_id] = button
|
||||
buttons[actual_button_id] = button
|
||||
config["buttons"] = buttons
|
||||
|
||||
await cls.save_config(db, config)
|
||||
@@ -905,11 +943,28 @@ class MenuLayoutService:
|
||||
) -> Optional[InlineKeyboardButton]:
|
||||
"""Построить кнопку из конфигурации."""
|
||||
button_type = button_config.get("type", "builtin")
|
||||
button_id = button_config.get("builtin_id") or button_config.get("id", "")
|
||||
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")
|
||||
icon = button_config.get("icon", "")
|
||||
|
||||
# Логирование для отладки кнопки connect
|
||||
is_connect_button = (
|
||||
button_id == "connect" or
|
||||
"connect" in str(button_id).lower() or
|
||||
action == "subscription_connect" or
|
||||
"connect" in str(action).lower()
|
||||
)
|
||||
|
||||
if is_connect_button:
|
||||
logger.info(
|
||||
f"🔗 Построение кнопки connect: "
|
||||
f"button_id={button_id}, type={button_type}, "
|
||||
f"open_mode={open_mode}, action={action}, "
|
||||
f"webapp_url={webapp_url}"
|
||||
)
|
||||
|
||||
# Получаем текст
|
||||
text = cls._get_localized_text(text_config, context.language)
|
||||
@@ -936,13 +991,51 @@ class MenuLayoutService:
|
||||
return InlineKeyboardButton(text=text, callback_data=action)
|
||||
else:
|
||||
# builtin - проверяем open_mode
|
||||
if open_mode == "direct" and webapp_url:
|
||||
if open_mode == "direct":
|
||||
# Прямое открытие Mini App через WebAppInfo
|
||||
return InlineKeyboardButton(
|
||||
text=text, web_app=types.WebAppInfo(url=webapp_url)
|
||||
)
|
||||
# Используем webapp_url, если указан, иначе action (если это URL)
|
||||
url = webapp_url or action
|
||||
|
||||
# Для кнопки connect: если URL не указан или это callback_data,
|
||||
# пытаемся получить URL из подписки пользователя
|
||||
if is_connect_button and (not url or not (url.startswith("http://") or url.startswith("https://"))):
|
||||
if context.subscription:
|
||||
from app.utils.subscription_utils import get_display_subscription_link
|
||||
subscription_url = get_display_subscription_link(context.subscription)
|
||||
if subscription_url:
|
||||
url = subscription_url
|
||||
logger.info(
|
||||
f"🔗 Кнопка connect: получен URL из подписки: {url[:50]}..."
|
||||
)
|
||||
# Если все еще нет URL, пробуем использовать настройку MINIAPP_CUSTOM_URL
|
||||
if not url or not (url.startswith("http://") or url.startswith("https://")):
|
||||
if settings.MINIAPP_CUSTOM_URL:
|
||||
url = settings.MINIAPP_CUSTOM_URL
|
||||
logger.info(
|
||||
f"🔗 Кнопка connect: использован MINIAPP_CUSTOM_URL: {url[:50]}..."
|
||||
)
|
||||
|
||||
# Проверяем, что это действительно URL
|
||||
if url and (url.startswith("http://") or url.startswith("https://")):
|
||||
logger.info(
|
||||
f"🔗 Кнопка connect: open_mode=direct, используем URL: {url[:50]}..."
|
||||
)
|
||||
return InlineKeyboardButton(
|
||||
text=text, web_app=types.WebAppInfo(url=url)
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"🔗 Кнопка connect: open_mode=direct, но URL не найден. "
|
||||
f"webapp_url={webapp_url}, action={action}, "
|
||||
f"subscription_url={'есть' if context.subscription else 'нет'}"
|
||||
)
|
||||
# Fallback на callback_data
|
||||
return InlineKeyboardButton(text=text, callback_data=action)
|
||||
else:
|
||||
# Стандартный callback_data
|
||||
logger.debug(
|
||||
f"Кнопка connect: open_mode={open_mode}, используем callback_data: {action}"
|
||||
)
|
||||
return InlineKeyboardButton(text=text, callback_data=action)
|
||||
|
||||
# --- Построение клавиатуры ---
|
||||
|
||||
139
tests/services/test_menu_layout_service.py
Normal file
139
tests/services/test_menu_layout_service.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""Тесты для MenuLayoutService."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from aiogram.types import InlineKeyboardButton
|
||||
|
||||
from app.services.menu_layout.service import MenuLayoutService
|
||||
from app.services.menu_layout.context import MenuContext
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_build_button_connect_direct_mode_with_url():
|
||||
"""Тест: кнопка connect с open_mode=direct и валидным URL должна создавать WebAppInfo."""
|
||||
button_config = {
|
||||
"type": "builtin",
|
||||
"builtin_id": "connect",
|
||||
"text": {"ru": "🔗 Подключиться"},
|
||||
"action": "subscription_connect",
|
||||
"open_mode": "direct",
|
||||
"webapp_url": "https://example.com/miniapp",
|
||||
}
|
||||
|
||||
context = MenuContext(
|
||||
language="ru",
|
||||
has_active_subscription=True,
|
||||
subscription_is_active=True,
|
||||
)
|
||||
|
||||
texts = MagicMock()
|
||||
texts.t = lambda key, default: default
|
||||
|
||||
button = MenuLayoutService._build_button(button_config, context, texts)
|
||||
|
||||
assert button is not None
|
||||
assert isinstance(button, InlineKeyboardButton)
|
||||
assert button.web_app is not None
|
||||
assert button.web_app.url == "https://example.com/miniapp"
|
||||
assert button.callback_data is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_build_button_connect_direct_mode_with_subscription_url():
|
||||
"""Тест: кнопка connect с open_mode=direct должна получать URL из подписки."""
|
||||
button_config = {
|
||||
"type": "builtin",
|
||||
"builtin_id": "connect",
|
||||
"text": {"ru": "🔗 Подключиться"},
|
||||
"action": "subscription_connect",
|
||||
"open_mode": "direct",
|
||||
"webapp_url": None,
|
||||
}
|
||||
|
||||
# Мокаем подписку с URL
|
||||
mock_subscription = MagicMock()
|
||||
mock_subscription.subscription_url = "https://subscription.example.com/link"
|
||||
mock_subscription.subscription_crypto_link = None
|
||||
|
||||
context = MenuContext(
|
||||
language="ru",
|
||||
has_active_subscription=True,
|
||||
subscription_is_active=True,
|
||||
subscription=mock_subscription,
|
||||
)
|
||||
|
||||
texts = MagicMock()
|
||||
texts.t = lambda key, default: default
|
||||
|
||||
with patch('app.utils.subscription_utils.get_display_subscription_link') as mock_get_link:
|
||||
mock_get_link.return_value = "https://subscription.example.com/link"
|
||||
|
||||
button = MenuLayoutService._build_button(button_config, context, texts)
|
||||
|
||||
assert button is not None
|
||||
assert isinstance(button, InlineKeyboardButton)
|
||||
assert button.web_app is not None
|
||||
assert button.web_app.url == "https://subscription.example.com/link"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_build_button_connect_callback_mode():
|
||||
"""Тест: кнопка connect с open_mode=callback должна создавать callback кнопку."""
|
||||
button_config = {
|
||||
"type": "builtin",
|
||||
"builtin_id": "connect",
|
||||
"text": {"ru": "🔗 Подключиться"},
|
||||
"action": "subscription_connect",
|
||||
"open_mode": "callback",
|
||||
"webapp_url": None,
|
||||
}
|
||||
|
||||
context = MenuContext(
|
||||
language="ru",
|
||||
has_active_subscription=True,
|
||||
subscription_is_active=True,
|
||||
)
|
||||
|
||||
texts = MagicMock()
|
||||
texts.t = lambda key, default: default
|
||||
|
||||
button = MenuLayoutService._build_button(button_config, context, texts)
|
||||
|
||||
assert button is not None
|
||||
assert isinstance(button, InlineKeyboardButton)
|
||||
assert button.callback_data == "subscription_connect"
|
||||
assert button.web_app is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_build_button_connect_direct_mode_fallback_to_callback():
|
||||
"""Тест: кнопка connect с open_mode=direct без URL должна fallback на callback."""
|
||||
button_config = {
|
||||
"type": "builtin",
|
||||
"builtin_id": "connect",
|
||||
"text": {"ru": "🔗 Подключиться"},
|
||||
"action": "subscription_connect",
|
||||
"open_mode": "direct",
|
||||
"webapp_url": None,
|
||||
}
|
||||
|
||||
context = MenuContext(
|
||||
language="ru",
|
||||
has_active_subscription=True,
|
||||
subscription_is_active=True,
|
||||
subscription=None, # Нет подписки
|
||||
)
|
||||
|
||||
texts = MagicMock()
|
||||
texts.t = lambda key, default: default
|
||||
|
||||
with patch('app.services.menu_layout.service.settings') as mock_settings:
|
||||
mock_settings.MINIAPP_CUSTOM_URL = None
|
||||
|
||||
button = MenuLayoutService._build_button(button_config, context, texts)
|
||||
|
||||
assert button is not None
|
||||
assert isinstance(button, InlineKeyboardButton)
|
||||
# Должен fallback на callback_data, так как URL не найден
|
||||
assert button.callback_data == "subscription_connect"
|
||||
|
||||
Reference in New Issue
Block a user