feat: open bot install guides in mini app
This commit is contained in:
@@ -167,6 +167,16 @@ SETTINGS_MANIFEST: List[SettingField] = [
|
||||
"Embedded install guides",
|
||||
"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(
|
||||
"SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED",
|
||||
"bool",
|
||||
|
||||
@@ -16,6 +16,10 @@ from bot.services.promo_code_service import PromoCodeService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from bot.states.user_states import UserPromoStates
|
||||
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 .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"),
|
||||
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(
|
||||
current_lang,
|
||||
i18n,
|
||||
settings,
|
||||
config_link_display,
|
||||
connect_button_url=connect_button_url,
|
||||
install_share_url=install_share_url,
|
||||
)
|
||||
else:
|
||||
await session.commit()
|
||||
|
||||
@@ -23,6 +23,10 @@ from bot.services.promo_code_service import PromoCodeService
|
||||
from bot.services.referral_service import ReferralService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
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 config.settings import Settings
|
||||
from db.dal import user_dal
|
||||
@@ -715,6 +719,23 @@ async def start_command_handler(
|
||||
),
|
||||
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
|
||||
|
||||
@@ -726,6 +747,7 @@ async def start_command_handler(
|
||||
settings,
|
||||
config_link_display,
|
||||
connect_button_url=connect_button_url,
|
||||
install_share_url=install_share_url,
|
||||
),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
@@ -27,6 +27,10 @@ from bot.keyboards.inline.user_keyboards import (
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
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 db.dal import subscription_dal, user_billing_dal
|
||||
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(
|
||||
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
|
||||
prepend_rows = []
|
||||
|
||||
# 1) Connect button: prefer the actual subscription URL; fall back to mini-app
|
||||
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(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
|
||||
@@ -14,6 +14,10 @@ from bot.services.notification_service import NotificationService
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
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 .start import send_main_menu
|
||||
@@ -74,6 +78,7 @@ async def request_trial_confirmation_handler(
|
||||
config_link_display_for_trial = None
|
||||
config_link_for_trial = None
|
||||
connect_button_url_for_trial = None
|
||||
install_share_url = None
|
||||
|
||||
if activation_result and activation_result.get("activated"):
|
||||
try:
|
||||
@@ -104,6 +109,14 @@ async def request_trial_confirmation_handler(
|
||||
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
|
||||
notification_service = NotificationService(callback.bot, settings, i18n)
|
||||
await notification_service.notify_trial_activation(user_id, end_date_obj)
|
||||
@@ -139,6 +152,7 @@ async def request_trial_confirmation_handler(
|
||||
settings,
|
||||
config_link_display_for_trial,
|
||||
connect_button_url=connect_button_url_for_trial,
|
||||
install_share_url=install_share_url,
|
||||
)
|
||||
if activation_result and activation_result.get("activated")
|
||||
else get_main_menu_inline_keyboard(
|
||||
@@ -214,6 +228,7 @@ async def confirm_activate_trial_handler(
|
||||
config_link_display_for_trial = None
|
||||
config_link_for_trial = None
|
||||
connect_button_url_for_trial = None
|
||||
install_share_url = None
|
||||
|
||||
if activation_result and activation_result.get("activated"):
|
||||
try:
|
||||
@@ -243,6 +258,13 @@ async def confirm_activate_trial_handler(
|
||||
config_link=config_link_for_trial,
|
||||
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:
|
||||
message_key_from_service = (
|
||||
activation_result.get("message_key", "trial_activation_failed")
|
||||
@@ -266,6 +288,7 @@ async def confirm_activate_trial_handler(
|
||||
settings,
|
||||
config_link_display_for_trial,
|
||||
connect_button_url=connect_button_url_for_trial,
|
||||
install_share_url=install_share_url,
|
||||
)
|
||||
if activation_result and activation_result.get("activated")
|
||||
else get_main_menu_inline_keyboard(
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
from aiogram.types import InlineKeyboardMarkup, WebAppInfo
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
|
||||
|
||||
from bot.utils.install_links import bot_install_guide_url
|
||||
from config.settings import Settings
|
||||
|
||||
BOT_MENU_CONTEXT = "bot"
|
||||
@@ -689,13 +690,29 @@ def get_connect_and_main_keyboard(
|
||||
config_link: Optional[str],
|
||||
connect_button_url: Optional[str] = None,
|
||||
preserve_message: bool = False,
|
||||
install_share_url: Optional[str] = None,
|
||||
) -> InlineKeyboardMarkup:
|
||||
"""Keyboard with a connect button and a back to main menu button."""
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
builder = InlineKeyboardBuilder()
|
||||
install_url = bot_install_guide_url(settings)
|
||||
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))
|
||||
elif settings.SUBSCRIPTION_MINI_APP_URL:
|
||||
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.services.notification_service import NotificationService
|
||||
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 db.dal import payment_dal, user_dal
|
||||
from db.models import Payment, User
|
||||
@@ -136,6 +140,7 @@ async def send_success_message_to_user(
|
||||
settings: Any,
|
||||
config_link_display: Optional[str],
|
||||
connect_button_url: Optional[str],
|
||||
install_share_url: Optional[str] = None,
|
||||
include_keyboard: bool = True,
|
||||
log_prefix: str = "payment_providers",
|
||||
) -> None:
|
||||
@@ -148,6 +153,7 @@ async def send_success_message_to_user(
|
||||
settings,
|
||||
config_link_display,
|
||||
connect_button_url=connect_button_url,
|
||||
install_share_url=install_share_url,
|
||||
preserve_message=True,
|
||||
)
|
||||
try:
|
||||
@@ -332,6 +338,31 @@ async def finalize_successful_payment(
|
||||
if req.text_prefix:
|
||||
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(
|
||||
bot=req.bot,
|
||||
user_id=req.user_id,
|
||||
@@ -341,6 +372,7 @@ async def finalize_successful_payment(
|
||||
settings=req.settings,
|
||||
config_link_display=config_link_display,
|
||||
connect_button_url=connect_button_url,
|
||||
install_share_url=install_share_url,
|
||||
include_keyboard=not req.skip_keyboard,
|
||||
log_prefix=req.log_prefix,
|
||||
)
|
||||
|
||||
@@ -36,6 +36,10 @@ from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.referral_service import ReferralService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
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 config.settings import Settings
|
||||
from db.dal import payment_dal, user_billing_dal, user_dal
|
||||
@@ -741,6 +745,16 @@ async def process_successful_payment(
|
||||
)
|
||||
)
|
||||
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(
|
||||
bot=bot,
|
||||
user_id=user_id,
|
||||
@@ -750,6 +764,7 @@ async def process_successful_payment(
|
||||
settings=settings,
|
||||
config_link_display=config_link_display,
|
||||
connect_button_url=connect_button_url,
|
||||
install_share_url=install_share_url,
|
||||
include_keyboard=include_keyboard,
|
||||
log_prefix="YooKassa webhook",
|
||||
)
|
||||
|
||||
@@ -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}"
|
||||
@@ -3,9 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
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 db.dal.subscription_dal import normalize_install_share_token
|
||||
|
||||
|
||||
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
|
||||
normalized = "premium" if str(kind or "").strip().lower() == "premium" else "regular"
|
||||
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)}"
|
||||
|
||||
@@ -327,6 +327,13 @@ class Settings(BaseSettings):
|
||||
default=True,
|
||||
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(
|
||||
default=True,
|
||||
description="Use Remnawave Panel Subscription Page config for embedded guides when available.",
|
||||
|
||||
@@ -50,6 +50,8 @@
|
||||
"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",
|
||||
"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",
|
||||
"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>",
|
||||
@@ -1618,6 +1620,8 @@
|
||||
"admin_settings_section_subscription_guides": "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_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_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",
|
||||
|
||||
@@ -50,6 +50,8 @@
|
||||
"yookassa_autopay_charge_initiated": "Запрос на списание с выбранной карты отправлен. Сообщим, как только платёж завершится.",
|
||||
"back_to_payment_methods_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})",
|
||||
"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} устройств. Подключить их можно через кнопку \"🔗 Подключиться\" в меню подписки.",
|
||||
@@ -1618,6 +1620,8 @@
|
||||
"admin_settings_section_subscription_guides": "Инструкции подключения",
|
||||
"admin_settings_field_subscription_guides_enabled_label": "Встроенные инструкции подключения",
|
||||
"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_description": "Брать Subscription Page config из Remnawave Panel по short UUID подписки пользователя.",
|
||||
"admin_settings_field_subscription_page_config_json_override_enabled_label": "Включить JSON-override из админки",
|
||||
|
||||
@@ -24,6 +24,7 @@ SUBSCRIPTION_PURCHASE_DESCRIPTION_SETTINGS = (
|
||||
|
||||
SUBSCRIPTION_GUIDE_SETTINGS = (
|
||||
"SUBSCRIPTION_GUIDES_ENABLED",
|
||||
"SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED",
|
||||
"SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED",
|
||||
"SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED",
|
||||
"SUBSCRIPTION_PAGE_CONFIG_PATH",
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
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
|
||||
|
||||
|
||||
@@ -39,3 +45,33 @@ class MiniAppUrlTests(unittest.TestCase):
|
||||
subscription_mini_app_topup_url(s, "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",
|
||||
)
|
||||
|
||||
@@ -41,6 +41,7 @@ class SettingsTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertTrue(settings.SUBSCRIPTION_GUIDES_ENABLED)
|
||||
self.assertFalse(settings.SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED)
|
||||
self.assertTrue(settings.SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED)
|
||||
self.assertFalse(settings.SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED)
|
||||
self.assertEqual(
|
||||
|
||||
@@ -8,6 +8,7 @@ from bot.handlers.user import referral
|
||||
from bot.handlers.user.subscription.core import _with_subscription_purchase_description
|
||||
from bot.keyboards.inline.user_keyboards import (
|
||||
get_bot_interface_inline_keyboard,
|
||||
get_connect_and_main_keyboard,
|
||||
get_information_links_keyboard,
|
||||
get_language_selection_keyboard,
|
||||
get_main_menu_inline_keyboard,
|
||||
@@ -41,6 +42,13 @@ class UserBotMenuTests(unittest.TestCase):
|
||||
TERMS_OF_SERVICE_URL="",
|
||||
TRIAL_ENABLED=True,
|
||||
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):
|
||||
@@ -70,6 +78,38 @@ class UserBotMenuTests(unittest.TestCase):
|
||||
self.assertIn("main_action:bot_info", 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):
|
||||
subscription_markup = get_subscription_options_keyboard(
|
||||
{1: 100},
|
||||
|
||||
Reference in New Issue
Block a user