feat: open bot install guides in mini app

This commit is contained in:
3252a8
2026-05-23 09:48:30 +03:00
parent 7c874cd4aa
commit ce0d4dccf7
17 changed files with 393 additions and 4 deletions
@@ -167,6 +167,16 @@ SETTINGS_MANIFEST: List[SettingField] = [
"Embedded install guides", "Embedded install guides",
"Open install instructions inside the Web App instead of an external connect page.", "Open install instructions inside the Web App instead of an external connect page.",
), ),
SettingField(
"SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED",
"bool",
"subscription_guides",
"Open install guides from bot",
(
"Use the Telegram Mini App install screen for bot connect buttons and show "
"public install guide links."
),
),
SettingField( SettingField(
"SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED", "SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED",
"bool", "bool",
+22
View File
@@ -16,6 +16,10 @@ from bot.services.promo_code_service import PromoCodeService
from bot.services.subscription_service import SubscriptionService from bot.services.subscription_service import SubscriptionService
from bot.states.user_states import UserPromoStates from bot.states.user_states import UserPromoStates
from bot.utils.callback_answer import safe_answer_callback from bot.utils.callback_answer import safe_answer_callback
from bot.utils.install_links import (
append_install_share_link_text,
ensure_user_install_guide_links,
)
from config.settings import Settings from config.settings import Settings
from .start import send_main_menu from .start import send_main_menu
@@ -160,12 +164,30 @@ async def process_promo_code_input(
end_date=(new_end_date.strftime("%d.%m.%Y %H:%M:%S") if new_end_date else "N/A"), end_date=(new_end_date.strftime("%d.%m.%Y %H:%M:%S") if new_end_date else "N/A"),
config_link=config_link_text, config_link=config_link_text,
) )
install_links = await ensure_user_install_guide_links(session, settings, user.id)
install_share_url = install_links.public_share_url
if install_share_url:
try:
await session.commit()
response_to_user_text = append_install_share_link_text(
response_to_user_text,
_,
install_share_url,
)
except Exception:
await session.rollback()
logging.exception(
"Failed to persist install guide share token for promo user %s.",
user.id,
)
install_share_url = None
reply_markup = get_connect_and_main_keyboard( reply_markup = get_connect_and_main_keyboard(
current_lang, current_lang,
i18n, i18n,
settings, settings,
config_link_display, config_link_display,
connect_button_url=connect_button_url, connect_button_url=connect_button_url,
install_share_url=install_share_url,
) )
else: else:
await session.commit() await session.commit()
+22
View File
@@ -23,6 +23,10 @@ from bot.services.promo_code_service import PromoCodeService
from bot.services.referral_service import ReferralService from bot.services.referral_service import ReferralService
from bot.services.subscription_service import SubscriptionService from bot.services.subscription_service import SubscriptionService
from bot.utils.callback_answer import safe_answer_callback from bot.utils.callback_answer import safe_answer_callback
from bot.utils.install_links import (
append_install_share_link_text,
ensure_user_install_guide_links,
)
from bot.utils.text_sanitizer import sanitize_display_name, sanitize_username from bot.utils.text_sanitizer import sanitize_display_name, sanitize_username
from config.settings import Settings from config.settings import Settings
from db.dal import user_dal from db.dal import user_dal
@@ -715,6 +719,23 @@ async def start_command_handler(
), ),
config_link=config_link_text, config_link=config_link_text,
) )
install_links = await ensure_user_install_guide_links(session, settings, user_id)
install_share_url = install_links.public_share_url
if install_share_url:
try:
await session.commit()
promo_success_text = append_install_share_link_text(
promo_success_text,
_,
install_share_url,
)
except Exception:
await session.rollback()
logging.exception(
"Failed to persist install guide share token for promo user %s.",
user_id,
)
install_share_url = None
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
@@ -726,6 +747,7 @@ async def start_command_handler(
settings, settings,
config_link_display, config_link_display,
connect_button_url=connect_button_url, connect_button_url=connect_button_url,
install_share_url=install_share_url,
), ),
parse_mode="HTML", parse_mode="HTML",
) )
+43 -1
View File
@@ -27,6 +27,10 @@ from bot.keyboards.inline.user_keyboards import (
from bot.middlewares.i18n import JsonI18n from bot.middlewares.i18n import JsonI18n
from bot.services.panel_api_service import PanelApiService from bot.services.panel_api_service import PanelApiService
from bot.services.subscription_service import SubscriptionService from bot.services.subscription_service import SubscriptionService
from bot.utils.install_links import (
append_install_share_link_text,
ensure_user_install_guide_links,
)
from config.settings import Settings from config.settings import Settings
from db.dal import subscription_dal, user_billing_dal from db.dal import subscription_dal, user_billing_dal
from db.models import Subscription from db.models import Subscription
@@ -1026,12 +1030,50 @@ async def my_subscription_command_handler(
local_sub = await subscription_dal.get_active_subscription_by_user_id( local_sub = await subscription_dal.get_active_subscription_by_user_id(
session, event.from_user.id session, event.from_user.id
) )
install_links = await ensure_user_install_guide_links(
session,
settings,
event.from_user.id,
local_subscription=local_sub,
)
install_url = install_links.personal_url
install_share_url = install_links.public_share_url
if install_share_url:
try:
await session.commit()
text = append_install_share_link_text(text, get_text, install_share_url)
except Exception:
await session.rollback()
logging.exception(
"Failed to persist install guide share token for user %s.",
event.from_user.id,
)
install_share_url = None
# Build rows to prepend above the base "back" markup # Build rows to prepend above the base "back" markup
prepend_rows = [] prepend_rows = []
# 1) Connect button: prefer the actual subscription URL; fall back to mini-app # 1) Connect button: prefer the actual subscription URL; fall back to mini-app
cfg_link_val = connect_button_url or config_link_display cfg_link_val = connect_button_url or config_link_display
if cfg_link_val: if install_url:
prepend_rows.append(
[
InlineKeyboardButton(
text=get_text("connect_button"),
web_app=WebAppInfo(url=install_url),
)
]
)
if install_share_url:
prepend_rows.append(
[
InlineKeyboardButton(
text=get_text("install_guide_share_button"),
url=install_share_url,
)
]
)
elif cfg_link_val:
prepend_rows.append( prepend_rows.append(
[ [
InlineKeyboardButton( InlineKeyboardButton(
@@ -14,6 +14,10 @@ from bot.services.notification_service import NotificationService
from bot.services.panel_api_service import PanelApiService from bot.services.panel_api_service import PanelApiService
from bot.services.subscription_service import SubscriptionService from bot.services.subscription_service import SubscriptionService
from bot.utils.config_link import prepare_config_links from bot.utils.config_link import prepare_config_links
from bot.utils.install_links import (
append_install_share_link_text,
ensure_user_install_guide_links,
)
from config.settings import Settings from config.settings import Settings
from .start import send_main_menu from .start import send_main_menu
@@ -74,6 +78,7 @@ async def request_trial_confirmation_handler(
config_link_display_for_trial = None config_link_display_for_trial = None
config_link_for_trial = None config_link_for_trial = None
connect_button_url_for_trial = None connect_button_url_for_trial = None
install_share_url = None
if activation_result and activation_result.get("activated"): if activation_result and activation_result.get("activated"):
try: try:
@@ -104,6 +109,14 @@ async def request_trial_confirmation_handler(
traffic_gb=traffic_display, traffic_gb=traffic_display,
) )
install_links = await ensure_user_install_guide_links(session, settings, user_id)
install_share_url = install_links.public_share_url
final_message_text_in_chat = append_install_share_link_text(
final_message_text_in_chat,
_,
install_share_url,
)
# Send notification to admin about new trial # Send notification to admin about new trial
notification_service = NotificationService(callback.bot, settings, i18n) notification_service = NotificationService(callback.bot, settings, i18n)
await notification_service.notify_trial_activation(user_id, end_date_obj) await notification_service.notify_trial_activation(user_id, end_date_obj)
@@ -139,6 +152,7 @@ async def request_trial_confirmation_handler(
settings, settings,
config_link_display_for_trial, config_link_display_for_trial,
connect_button_url=connect_button_url_for_trial, connect_button_url=connect_button_url_for_trial,
install_share_url=install_share_url,
) )
if activation_result and activation_result.get("activated") if activation_result and activation_result.get("activated")
else get_main_menu_inline_keyboard( else get_main_menu_inline_keyboard(
@@ -214,6 +228,7 @@ async def confirm_activate_trial_handler(
config_link_display_for_trial = None config_link_display_for_trial = None
config_link_for_trial = None config_link_for_trial = None
connect_button_url_for_trial = None connect_button_url_for_trial = None
install_share_url = None
if activation_result and activation_result.get("activated"): if activation_result and activation_result.get("activated"):
try: try:
@@ -243,6 +258,13 @@ async def confirm_activate_trial_handler(
config_link=config_link_for_trial, config_link=config_link_for_trial,
traffic_gb=traffic_display, traffic_gb=traffic_display,
) )
install_links = await ensure_user_install_guide_links(session, settings, user_id)
install_share_url = install_links.public_share_url
final_message_text_in_chat = append_install_share_link_text(
final_message_text_in_chat,
_,
install_share_url,
)
else: else:
message_key_from_service = ( message_key_from_service = (
activation_result.get("message_key", "trial_activation_failed") activation_result.get("message_key", "trial_activation_failed")
@@ -266,6 +288,7 @@ async def confirm_activate_trial_handler(
settings, settings,
config_link_display_for_trial, config_link_display_for_trial,
connect_button_url=connect_button_url_for_trial, connect_button_url=connect_button_url_for_trial,
install_share_url=install_share_url,
) )
if activation_result and activation_result.get("activated") if activation_result and activation_result.get("activated")
else get_main_menu_inline_keyboard( else get_main_menu_inline_keyboard(
+18 -1
View File
@@ -3,6 +3,7 @@ from typing import Any, Dict, List, Optional, Tuple
from aiogram.types import InlineKeyboardMarkup, WebAppInfo from aiogram.types import InlineKeyboardMarkup, WebAppInfo
from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
from bot.utils.install_links import bot_install_guide_url
from config.settings import Settings from config.settings import Settings
BOT_MENU_CONTEXT = "bot" BOT_MENU_CONTEXT = "bot"
@@ -689,13 +690,29 @@ def get_connect_and_main_keyboard(
config_link: Optional[str], config_link: Optional[str],
connect_button_url: Optional[str] = None, connect_button_url: Optional[str] = None,
preserve_message: bool = False, preserve_message: bool = False,
install_share_url: Optional[str] = None,
) -> InlineKeyboardMarkup: ) -> InlineKeyboardMarkup:
"""Keyboard with a connect button and a back to main menu button.""" """Keyboard with a connect button and a back to main menu button."""
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
install_url = bot_install_guide_url(settings)
button_target = connect_button_url or config_link button_target = connect_button_url or config_link
if button_target: if install_url:
builder.row(
InlineKeyboardButton(
text=_("connect_button"),
web_app=WebAppInfo(url=install_url),
)
)
if install_share_url:
builder.row(
InlineKeyboardButton(
text=_("install_guide_share_button"),
url=install_share_url,
)
)
elif button_target:
builder.row(InlineKeyboardButton(text=_("connect_button"), url=button_target)) builder.row(InlineKeyboardButton(text=_("connect_button"), url=button_target))
elif settings.SUBSCRIPTION_MINI_APP_URL: elif settings.SUBSCRIPTION_MINI_APP_URL:
builder.row( builder.row(
@@ -11,6 +11,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
from bot.services.notification_service import NotificationService from bot.services.notification_service import NotificationService
from bot.utils.config_link import prepare_config_links from bot.utils.config_link import prepare_config_links
from bot.utils.install_links import (
append_install_share_link_text,
ensure_user_install_guide_links,
)
from bot.utils.text_sanitizer import sanitize_display_name, username_for_display from bot.utils.text_sanitizer import sanitize_display_name, username_for_display
from db.dal import payment_dal, user_dal from db.dal import payment_dal, user_dal
from db.models import Payment, User from db.models import Payment, User
@@ -136,6 +140,7 @@ async def send_success_message_to_user(
settings: Any, settings: Any,
config_link_display: Optional[str], config_link_display: Optional[str],
connect_button_url: Optional[str], connect_button_url: Optional[str],
install_share_url: Optional[str] = None,
include_keyboard: bool = True, include_keyboard: bool = True,
log_prefix: str = "payment_providers", log_prefix: str = "payment_providers",
) -> None: ) -> None:
@@ -148,6 +153,7 @@ async def send_success_message_to_user(
settings, settings,
config_link_display, config_link_display,
connect_button_url=connect_button_url, connect_button_url=connect_button_url,
install_share_url=install_share_url,
preserve_message=True, preserve_message=True,
) )
try: try:
@@ -332,6 +338,31 @@ async def finalize_successful_payment(
if req.text_prefix: if req.text_prefix:
success_text = f"{req.text_prefix}\n{success_text}" success_text = f"{req.text_prefix}\n{success_text}"
install_share_url = None
if not req.skip_keyboard:
install_links = await ensure_user_install_guide_links(
req.session,
req.settings,
req.user_id,
)
install_share_url = install_links.public_share_url
if install_share_url:
try:
await req.session.commit()
success_text = append_install_share_link_text(
success_text,
translator,
install_share_url,
)
except Exception:
await req.session.rollback()
logging.exception(
"%s: failed to persist install guide share token for user %s.",
req.log_prefix,
req.user_id,
)
install_share_url = None
await send_success_message_to_user( await send_success_message_to_user(
bot=req.bot, bot=req.bot,
user_id=req.user_id, user_id=req.user_id,
@@ -341,6 +372,7 @@ async def finalize_successful_payment(
settings=req.settings, settings=req.settings,
config_link_display=config_link_display, config_link_display=config_link_display,
connect_button_url=connect_button_url, connect_button_url=connect_button_url,
install_share_url=install_share_url,
include_keyboard=not req.skip_keyboard, include_keyboard=not req.skip_keyboard,
log_prefix=req.log_prefix, log_prefix=req.log_prefix,
) )
+15
View File
@@ -36,6 +36,10 @@ from bot.services.panel_api_service import PanelApiService
from bot.services.referral_service import ReferralService from bot.services.referral_service import ReferralService
from bot.services.subscription_service import SubscriptionService from bot.services.subscription_service import SubscriptionService
from bot.utils.config_link import prepare_config_links from bot.utils.config_link import prepare_config_links
from bot.utils.install_links import (
append_install_share_link_text,
ensure_user_install_guide_links,
)
from bot.utils.request_security import ip_in_allowlist, request_client_ip from bot.utils.request_security import ip_in_allowlist, request_client_ip
from config.settings import Settings from config.settings import Settings
from db.dal import payment_dal, user_billing_dal, user_dal from db.dal import payment_dal, user_billing_dal, user_dal
@@ -741,6 +745,16 @@ async def process_successful_payment(
) )
) )
include_keyboard = True include_keyboard = True
install_share_url = None
if include_keyboard:
install_links = await ensure_user_install_guide_links(session, settings, user_id)
install_share_url = install_links.public_share_url
details_message = append_install_share_link_text(
details_message,
translator,
install_share_url,
)
await send_success_message_to_user( await send_success_message_to_user(
bot=bot, bot=bot,
user_id=user_id, user_id=user_id,
@@ -750,6 +764,7 @@ async def process_successful_payment(
settings=settings, settings=settings,
config_link_display=config_link_display, config_link_display=config_link_display,
connect_button_url=connect_button_url, connect_button_url=connect_button_url,
install_share_url=install_share_url,
include_keyboard=include_keyboard, include_keyboard=include_keyboard,
log_prefix="YooKassa webhook", log_prefix="YooKassa webhook",
) )
+84
View File
@@ -0,0 +1,84 @@
"""Helpers for Telegram bot install-guide links."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Any, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from bot.utils.mini_app_url import (
subscription_mini_app_install_url,
subscription_public_install_url,
)
from config.subscription_guides_config import subscription_guides_available
from db.dal import subscription_dal
@dataclass(frozen=True)
class InstallGuideLinks:
personal_url: Optional[str] = None
public_share_url: Optional[str] = None
def bot_install_guides_enabled(settings: Any) -> bool:
return bool(
getattr(settings, "SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED", False)
and subscription_guides_available(settings)
and subscription_mini_app_install_url(settings)
)
def bot_install_guide_url(settings: Any) -> Optional[str]:
if not bot_install_guides_enabled(settings):
return None
return subscription_mini_app_install_url(settings)
async def ensure_user_install_guide_links(
session: AsyncSession,
settings: Any,
user_id: int,
panel_user_uuid: Optional[str] = None,
local_subscription: Optional[Any] = None,
) -> InstallGuideLinks:
personal_url = bot_install_guide_url(settings)
if not personal_url:
return InstallGuideLinks()
public_share_url = None
try:
local_sub = (
local_subscription
if local_subscription is not None
else await subscription_dal.get_active_subscription_by_user_id(
session,
user_id,
panel_user_uuid,
)
)
if local_sub is not None:
share_token = await subscription_dal.ensure_install_share_token(session, local_sub)
public_share_url = subscription_public_install_url(settings, share_token)
except Exception:
logging.exception("Failed to resolve install guide share link for user %s.", user_id)
return InstallGuideLinks(personal_url=personal_url, public_share_url=public_share_url)
def append_install_share_link_text(
text: str,
translator: Any,
public_share_url: Optional[str],
) -> str:
if not public_share_url:
return text
try:
share_line = translator(
"install_guide_share_link_line",
install_share_link=public_share_url,
)
except Exception:
share_line = f"\n\nInstall guide:\n<code>{public_share_url}</code>"
return f"{text}{share_line}"
+30 -1
View File
@@ -3,9 +3,10 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import Optional
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit from urllib.parse import parse_qsl, quote, urlencode, urlsplit, urlunsplit
from config.settings import Settings from config.settings import Settings
from db.dal.subscription_dal import normalize_install_share_token
def append_query_params(base_url: str, params: dict[str, str]) -> str: def append_query_params(base_url: str, params: dict[str, str]) -> str:
@@ -31,3 +32,31 @@ def subscription_mini_app_topup_url(settings: Settings, kind: str) -> Optional[s
return None return None
normalized = "premium" if str(kind or "").strip().lower() == "premium" else "regular" normalized = "premium" if str(kind or "").strip().lower() == "premium" else "regular"
return append_query_params(base, {"topup": normalized}) return append_query_params(base, {"topup": normalized})
def subscription_mini_app_path_url(settings: Settings, path: str) -> Optional[str]:
"""Return a Mini App URL with ``path`` appended to the configured app base."""
base = str(getattr(settings, "SUBSCRIPTION_MINI_APP_URL", None) or "").strip()
if not base:
return None
normalized_path = f"/{str(path or '').lstrip('/')}"
return f"{base.rstrip('/')}{normalized_path}"
def subscription_mini_app_install_url(settings: Settings) -> Optional[str]:
"""Return the personal embedded install guide URL."""
return subscription_mini_app_path_url(settings, "/install")
def subscription_public_install_url(settings: Settings, share_token: str) -> Optional[str]:
"""Return the public install guide URL for a normalized share token."""
token = normalize_install_share_token(share_token)
base = str(getattr(settings, "SUBSCRIPTION_MINI_APP_URL", None) or "").strip()
if not token or not base:
return None
parts = urlsplit(base)
if parts.scheme and parts.netloc:
public_base = urlunsplit((parts.scheme, parts.netloc, "", "", ""))
else:
public_base = base.rstrip("/")
return f"{public_base.rstrip('/')}/s/{quote(token)}"
+7
View File
@@ -327,6 +327,13 @@ class Settings(BaseSettings):
default=True, default=True,
description="Show embedded install instructions inside the subscription Mini App.", description="Show embedded install instructions inside the subscription Mini App.",
) )
SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED: bool = Field(
default=False,
description=(
"Open Mini App install guides from Telegram bot connect buttons and show public "
"install guide share links."
),
)
SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED: bool = Field( SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED: bool = Field(
default=True, default=True,
description="Use Remnawave Panel Subscription Page config for embedded guides when available.", description="Use Remnawave Panel Subscription Page config for embedded guides when available.",
+4
View File
@@ -50,6 +50,8 @@
"yookassa_autopay_charge_initiated": "Charge request sent to the selected card. We'll notify you once the payment completes.", "yookassa_autopay_charge_initiated": "Charge request sent to the selected card. We'll notify you once the payment completes.",
"back_to_payment_methods_button": "⬅️ Back", "back_to_payment_methods_button": "⬅️ Back",
"connect_button": "🔗 Connect", "connect_button": "🔗 Connect",
"install_guide_share_button": "🔗 Share install guide",
"install_guide_share_link_line": "\n\nInstall guide for sharing:\n<code>{install_share_link}</code>",
"cancel_button": "❌ Cancel", "cancel_button": "❌ Cancel",
"devices_button": "📱 My Devices ({current_devices}/{max_devices})", "devices_button": "📱 My Devices ({current_devices}/{max_devices})",
"my_devices_details": "📱 <b>My Devices ({current_devices}/{max_devices})</b>\n\n{devices}\n\nYou can disconnect a device by selecting it from the list below.\n<blockquote><i>Note: If you disconnect a device, it will be automatically connected again when you use it next. Before deleting, make sure you have deleted the subscription from the application.</i></blockquote>", "my_devices_details": "📱 <b>My Devices ({current_devices}/{max_devices})</b>\n\n{devices}\n\nYou can disconnect a device by selecting it from the list below.\n<blockquote><i>Note: If you disconnect a device, it will be automatically connected again when you use it next. Before deleting, make sure you have deleted the subscription from the application.</i></blockquote>",
@@ -1618,6 +1620,8 @@
"admin_settings_section_subscription_guides": "Install guides", "admin_settings_section_subscription_guides": "Install guides",
"admin_settings_field_subscription_guides_enabled_label": "Embedded install guides", "admin_settings_field_subscription_guides_enabled_label": "Embedded install guides",
"admin_settings_field_subscription_guides_enabled_description": "Open install instructions inside the Web App instead of an external connect page.", "admin_settings_field_subscription_guides_enabled_description": "Open install instructions inside the Web App instead of an external connect page.",
"admin_settings_field_subscription_guides_bot_menu_enabled_label": "Open install guides from bot",
"admin_settings_field_subscription_guides_bot_menu_enabled_description": "Use the Telegram Mini App install screen for bot connect buttons and show public install guide links.",
"admin_settings_field_subscription_page_config_panel_enabled_label": "Use Remnawave Panel config", "admin_settings_field_subscription_page_config_panel_enabled_label": "Use Remnawave Panel config",
"admin_settings_field_subscription_page_config_panel_enabled_description": "Fetch Subscription Page config from Remnawave Panel by the user's subscription short UUID.", "admin_settings_field_subscription_page_config_panel_enabled_description": "Fetch Subscription Page config from Remnawave Panel by the user's subscription short UUID.",
"admin_settings_field_subscription_page_config_json_override_enabled_label": "Enable admin JSON override", "admin_settings_field_subscription_page_config_json_override_enabled_label": "Enable admin JSON override",
+4
View File
@@ -50,6 +50,8 @@
"yookassa_autopay_charge_initiated": "Запрос на списание с выбранной карты отправлен. Сообщим, как только платёж завершится.", "yookassa_autopay_charge_initiated": "Запрос на списание с выбранной карты отправлен. Сообщим, как только платёж завершится.",
"back_to_payment_methods_button": "⬅️ Назад", "back_to_payment_methods_button": "⬅️ Назад",
"connect_button": "🔗 Подключиться", "connect_button": "🔗 Подключиться",
"install_guide_share_button": "🔗 Поделиться инструкцией",
"install_guide_share_link_line": "\n\nИнструкция для передачи:\n<code>{install_share_link}</code>",
"devices_button": "📱 Мои устройства ({current_devices}/{max_devices})", "devices_button": "📱 Мои устройства ({current_devices}/{max_devices})",
"my_devices_details": "📱 <b>Список ваших устройств ({current_devices}/{max_devices})</b>\n\n{devices}\n\nВы можете отключить устройство, выбрав его в списке ниже.\n<blockquote><i>Примечание: Если вы отключили устройство, оно будет автоматически подключено заново при следующем использовании. Перед удалением убедитесь, что вы удалили подписку из приложения.</i></blockquote>", "my_devices_details": "📱 <b>Список ваших устройств ({current_devices}/{max_devices})</b>\n\n{devices}\n\nВы можете отключить устройство, выбрав его в списке ниже.\n<blockquote><i>Примечание: Если вы отключили устройство, оно будет автоматически подключено заново при следующем использовании. Перед удалением убедитесь, что вы удалили подписку из приложения.</i></blockquote>",
"no_devices_details_found_message": "📱 <b>Список ваших устройств</b>\n\nУ вас пока нет устройств.\nВам доступно {max_devices} устройств. Подключить их можно через кнопку \"🔗 Подключиться\" в меню подписки.", "no_devices_details_found_message": "📱 <b>Список ваших устройств</b>\n\nУ вас пока нет устройств.\nВам доступно {max_devices} устройств. Подключить их можно через кнопку \"🔗 Подключиться\" в меню подписки.",
@@ -1618,6 +1620,8 @@
"admin_settings_section_subscription_guides": "Инструкции подключения", "admin_settings_section_subscription_guides": "Инструкции подключения",
"admin_settings_field_subscription_guides_enabled_label": "Встроенные инструкции подключения", "admin_settings_field_subscription_guides_enabled_label": "Встроенные инструкции подключения",
"admin_settings_field_subscription_guides_enabled_description": "Открывать инструкции прямо внутри Web App вместо внешней страницы подключения.", "admin_settings_field_subscription_guides_enabled_description": "Открывать инструкции прямо внутри Web App вместо внешней страницы подключения.",
"admin_settings_field_subscription_guides_bot_menu_enabled_label": "Открывать инструкции из бота",
"admin_settings_field_subscription_guides_bot_menu_enabled_description": "Открывать экран установки в Telegram Mini App для кнопок подключения в боте и показывать публичные ссылки на инструкцию.",
"admin_settings_field_subscription_page_config_panel_enabled_label": "Использовать конфиг Remnawave Panel", "admin_settings_field_subscription_page_config_panel_enabled_label": "Использовать конфиг Remnawave Panel",
"admin_settings_field_subscription_page_config_panel_enabled_description": "Брать Subscription Page config из Remnawave Panel по short UUID подписки пользователя.", "admin_settings_field_subscription_page_config_panel_enabled_description": "Брать Subscription Page config из Remnawave Panel по short UUID подписки пользователя.",
"admin_settings_field_subscription_page_config_json_override_enabled_label": "Включить JSON-override из админки", "admin_settings_field_subscription_page_config_json_override_enabled_label": "Включить JSON-override из админки",
@@ -24,6 +24,7 @@ SUBSCRIPTION_PURCHASE_DESCRIPTION_SETTINGS = (
SUBSCRIPTION_GUIDE_SETTINGS = ( SUBSCRIPTION_GUIDE_SETTINGS = (
"SUBSCRIPTION_GUIDES_ENABLED", "SUBSCRIPTION_GUIDES_ENABLED",
"SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED",
"SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED", "SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED",
"SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED", "SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED",
"SUBSCRIPTION_PAGE_CONFIG_PATH", "SUBSCRIPTION_PAGE_CONFIG_PATH",
+37 -1
View File
@@ -1,6 +1,12 @@
import unittest import unittest
from bot.utils.mini_app_url import append_query_params, subscription_mini_app_topup_url from bot.utils.mini_app_url import (
append_query_params,
subscription_mini_app_install_url,
subscription_mini_app_path_url,
subscription_mini_app_topup_url,
subscription_public_install_url,
)
from config.settings import Settings from config.settings import Settings
@@ -39,3 +45,33 @@ class MiniAppUrlTests(unittest.TestCase):
subscription_mini_app_topup_url(s, "regular"), subscription_mini_app_topup_url(s, "regular"),
"https://app.example.com/webapp?topup=regular", "https://app.example.com/webapp?topup=regular",
) )
def test_subscription_mini_app_path_url(self):
s = Settings(
_env_file=None,
BOT_TOKEN="x",
POSTGRES_USER="u",
POSTGRES_PASSWORD="p",
SUBSCRIPTION_MINI_APP_URL="https://app.example.com/webapp/",
)
self.assertEqual(
subscription_mini_app_path_url(s, "/install"),
"https://app.example.com/webapp/install",
)
self.assertEqual(
subscription_mini_app_install_url(s),
"https://app.example.com/webapp/install",
)
def test_subscription_public_install_url_uses_origin(self):
s = Settings(
_env_file=None,
BOT_TOKEN="x",
POSTGRES_USER="u",
POSTGRES_PASSWORD="p",
SUBSCRIPTION_MINI_APP_URL="https://app.example.com/webapp",
)
self.assertEqual(
subscription_public_install_url(s, "8f559061460e8fede78ef18dce887236"),
"https://app.example.com/s/8f559061460e8fede78ef18dce887236",
)
+1
View File
@@ -41,6 +41,7 @@ class SettingsTests(unittest.TestCase):
) )
self.assertTrue(settings.SUBSCRIPTION_GUIDES_ENABLED) self.assertTrue(settings.SUBSCRIPTION_GUIDES_ENABLED)
self.assertFalse(settings.SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED)
self.assertTrue(settings.SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED) self.assertTrue(settings.SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED)
self.assertFalse(settings.SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED) self.assertFalse(settings.SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED)
self.assertEqual( self.assertEqual(
+40
View File
@@ -8,6 +8,7 @@ from bot.handlers.user import referral
from bot.handlers.user.subscription.core import _with_subscription_purchase_description from bot.handlers.user.subscription.core import _with_subscription_purchase_description
from bot.keyboards.inline.user_keyboards import ( from bot.keyboards.inline.user_keyboards import (
get_bot_interface_inline_keyboard, get_bot_interface_inline_keyboard,
get_connect_and_main_keyboard,
get_information_links_keyboard, get_information_links_keyboard,
get_language_selection_keyboard, get_language_selection_keyboard,
get_main_menu_inline_keyboard, get_main_menu_inline_keyboard,
@@ -41,6 +42,13 @@ class UserBotMenuTests(unittest.TestCase):
TERMS_OF_SERVICE_URL="", TERMS_OF_SERVICE_URL="",
TRIAL_ENABLED=True, TRIAL_ENABLED=True,
SERVER_STATUS_URL="", SERVER_STATUS_URL="",
SUBSCRIPTION_GUIDES_ENABLED=True,
SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED=False,
SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED=True,
SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED=False,
SUBSCRIPTION_PAGE_CONFIG_JSON="",
PANEL_API_URL="https://panel.example.com",
PANEL_API_KEY="token",
) )
def _callback_data(self, markup): def _callback_data(self, markup):
@@ -70,6 +78,38 @@ class UserBotMenuTests(unittest.TestCase):
self.assertIn("main_action:bot_info", callbacks) self.assertIn("main_action:bot_info", callbacks)
self.assertIn("main_action:back_to_main", callbacks) self.assertIn("main_action:back_to_main", callbacks)
def test_connect_keyboard_uses_subscription_url_when_bot_guides_disabled(self):
markup = get_connect_and_main_keyboard(
"en",
self.i18n,
self.settings,
"https://sb.example.com/user",
connect_button_url=None,
)
self.assertEqual(markup.inline_keyboard[0][0].url, "https://sb.example.com/user")
self.assertIsNone(markup.inline_keyboard[0][0].web_app)
def test_connect_keyboard_opens_install_guide_when_bot_guides_enabled(self):
self.settings.SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED = True
markup = get_connect_and_main_keyboard(
"en",
self.i18n,
self.settings,
"https://sb.example.com/user",
install_share_url="https://app.example.com/s/8f559061460e8fede78ef18dce887236",
)
self.assertIsNone(markup.inline_keyboard[0][0].url)
self.assertEqual(
markup.inline_keyboard[0][0].web_app.url,
"https://app.example.com/install",
)
self.assertEqual(
markup.inline_keyboard[1][0].url,
"https://app.example.com/s/8f559061460e8fede78ef18dce887236",
)
def test_nested_bot_menu_keyboards_can_target_bot_interface_back(self): def test_nested_bot_menu_keyboards_can_target_bot_interface_back(self):
subscription_markup = get_subscription_options_keyboard( subscription_markup = get_subscription_options_keyboard(
{1: 100}, {1: 100},