fix: guide mode buttons — support external type alias, extract urlScheme from blocks

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
This commit is contained in:
Fringg
2026-05-04 05:04:27 +03:00
parent 51dfc3a1a2
commit 2385814d77
2 changed files with 56 additions and 8 deletions
+27 -1
View File
@@ -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]: def normalize_app(app: dict[str, Any]) -> dict[str, Any]:
"""Normalize Remnawave app dict to a unified format with blocks.""" """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 { return {
'id': app.get('id', app.get('name', 'unknown')), 'id': app.get('id', app.get('name', 'unknown')),
'name': app.get('name', ''), 'name': app.get('name', ''),
'isFeatured': app.get('featured', app.get('isFeatured', False)), 'isFeatured': app.get('featured', app.get('isFeatured', False)),
'urlScheme': app.get('urlScheme', ''), 'urlScheme': url_scheme,
'isNeedBase64Encoding': app.get('isNeedBase64Encoding', False), 'isNeedBase64Encoding': app.get('isNeedBase64Encoding', False),
'blocks': app.get('blocks', []), 'blocks': app.get('blocks', []),
'_raw': app, '_raw': app,
+29 -7
View File
@@ -8,6 +8,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import PERIOD_PRICES, settings from app.config import PERIOD_PRICES, settings
from app.database.models import User 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.loader import DEFAULT_LANGUAGE
from app.localization.texts import get_texts from app.localization.texts import get_texts
from app.utils.miniapp_buttons import build_miniapp_or_callback_button 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, platforms: list[dict] | None = None,
sub_id: int | None = None, sub_id: int | None = None,
) -> InlineKeyboardMarkup: ) -> InlineKeyboardMarkup:
from app.config import settings
from app.handlers.subscription.common import get_localized_value
texts = get_texts(language) texts = get_texts(language)
back_cb = f'sm:{sub_id}' if sub_id and settings.is_multi_tariff_enabled() else 'menu_subscription' 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, has_other_apps: bool = False,
sub_id: int | None = None, sub_id: int | None = None,
) -> InlineKeyboardMarkup: ) -> InlineKeyboardMarkup:
from app.handlers.subscription.common import create_deep_link, get_localized_value, resolve_button_url
texts = get_texts(language) texts = get_texts(language)
back_cb = f'sm:{sub_id}' if sub_id and settings.is_multi_tariff_enabled() else 'menu_subscription' 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): if not isinstance(btn, dict):
continue continue
btn_type = btn.get('type', '') 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', {}) btn_text = btn.get('text', {})
if isinstance(btn_text, dict): if isinstance(btn_text, dict):
btn_text = get_localized_value(btn_text, language) btn_text = get_localized_value(btn_text, language)
@@ -2644,9 +2649,26 @@ def get_connection_guide_keyboard(
] ]
) )
elif btn_type == 'subscriptionLink': elif btn_type == 'subscriptionLink':
# First try to resolve the button's URL template
url = resolved_url or resolve_button_url(btn_url, subscription_url) 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: if final_url:
keyboard.append( keyboard.append(
[ [