From 2385814d77c6b847fb0df248b5aaaa2ff27fd20e Mon Sep 17 00:00:00 2001 From: Fringg Date: Mon, 4 May 2026 05:04:27 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20guide=20mode=20buttons=20=E2=80=94=20sup?= =?UTF-8?q?port=20external=20type=20alias,=20extract=20urlScheme=20from=20?= =?UTF-8?q?blocks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From PR #2923 by @dotX12, with improvements: - Support type: "external" as alias for "externalLink" in app config - Extract urlScheme from subscriptionLink buttons in blocks[] when not at root - Wrap custom URL schemes in HTTPS redirect for Telegram compatibility - Fallback to plain subscription URL when no redirect template configured Improvements over original PR: - Also check btn.get('url') not just btn.get('link') for scheme extraction - Validate extracted scheme contains :// before accepting - Skip redundant redirect wrapping when create_deep_link already wrapped --- app/handlers/subscription/common.py | 28 +++++++++++++++++++++- app/keyboards/inline.py | 36 +++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/app/handlers/subscription/common.py b/app/handlers/subscription/common.py index c33f86b2..9c2d2146 100644 --- a/app/handlers/subscription/common.py +++ b/app/handlers/subscription/common.py @@ -406,11 +406,37 @@ async def get_apps_for_platform_async(device_type: str, language: str = 'ru') -> def normalize_app(app: dict[str, Any]) -> dict[str, Any]: """Normalize Remnawave app dict to a unified format with blocks.""" + + # Extract urlScheme from blocks if not present at root level + url_scheme = app.get('urlScheme', '') + + if not url_scheme: + # Try to extract from subscriptionLink button in blocks + blocks = app.get('blocks', []) + for block in blocks: + if not isinstance(block, dict): + continue + buttons = block.get('buttons', []) + for btn in buttons: + if not isinstance(btn, dict): + continue + if btn.get('type') == 'subscriptionLink': + link = btn.get('link', '') or btn.get('url', '') + if '{{SUBSCRIPTION_LINK}}' in link: + url_scheme = link.split('{{SUBSCRIPTION_LINK}}')[0] + break + if url_scheme: + break + + # Validate extracted scheme contains :// + if url_scheme and '://' not in url_scheme: + url_scheme = '' + return { 'id': app.get('id', app.get('name', 'unknown')), 'name': app.get('name', ''), 'isFeatured': app.get('featured', app.get('isFeatured', False)), - 'urlScheme': app.get('urlScheme', ''), + 'urlScheme': url_scheme, 'isNeedBase64Encoding': app.get('isNeedBase64Encoding', False), 'blocks': app.get('blocks', []), '_raw': app, diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index 5b6dc0d9..7b9a676b 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -8,6 +8,12 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.config import PERIOD_PRICES, settings from app.database.models import User +from app.handlers.subscription.common import ( + build_redirect_link, + create_deep_link, + get_localized_value, + resolve_button_url, +) from app.localization.loader import DEFAULT_LANGUAGE from app.localization.texts import get_texts from app.utils.miniapp_buttons import build_miniapp_or_callback_button @@ -2554,9 +2560,6 @@ def get_device_selection_keyboard( platforms: list[dict] | None = None, sub_id: int | None = None, ) -> InlineKeyboardMarkup: - from app.config import settings - from app.handlers.subscription.common import get_localized_value - texts = get_texts(language) back_cb = f'sm:{sub_id}' if sub_id and settings.is_multi_tariff_enabled() else 'menu_subscription' @@ -2609,8 +2612,6 @@ def get_connection_guide_keyboard( has_other_apps: bool = False, sub_id: int | None = None, ) -> InlineKeyboardMarkup: - from app.handlers.subscription.common import create_deep_link, get_localized_value, resolve_button_url - texts = get_texts(language) back_cb = f'sm:{sub_id}' if sub_id and settings.is_multi_tariff_enabled() else 'menu_subscription' @@ -2623,6 +2624,10 @@ def get_connection_guide_keyboard( if not isinstance(btn, dict): continue btn_type = btn.get('type', '') + # Support both 'external' and 'externalLink' for backward compatibility + if btn_type == 'external': + btn_type = 'externalLink' + btn_text = btn.get('text', {}) if isinstance(btn_text, dict): btn_text = get_localized_value(btn_text, language) @@ -2644,9 +2649,26 @@ def get_connection_guide_keyboard( ] ) elif btn_type == 'subscriptionLink': + # First try to resolve the button's URL template url = resolved_url or resolve_button_url(btn_url, subscription_url) - deep_link = create_deep_link(app.get('_raw', app), subscription_url) - final_url = deep_link or url or subscription_url + + # If button has no template, try deep link + if not btn_url or '{{SUBSCRIPTION_LINK}}' not in btn_url: + deep_link = create_deep_link(app.get('_raw', app), subscription_url) + final_url = deep_link or url or subscription_url + else: + final_url = url or subscription_url + + # Telegram doesn't support custom URL schemes — wrap with redirect + if final_url and not final_url.startswith(('http://', 'https://')): + template = settings.get_happ_cryptolink_redirect_template() + if template: + wrapped_url = build_redirect_link(final_url, template) + if wrapped_url: + final_url = wrapped_url + else: + final_url = subscription_url + if final_url: keyboard.append( [