295d2e877e
Replace dual-configuration architecture (Remnawave API + local file fallback) with Remnawave-only approach. When config is unavailable, show explicit "not configured" message instead of silent file fallback. - Delete app-config.json and admin_apps.py CRUD module (~1260 lines) - Remove sync loaders, legacy step format handlers, device_mapping dicts - Remove miniapp /app-config.json endpoint and filesystem search - Remove backup service app-config.json snapshot/restore - Remove APP_CONFIG_PATH setting, env var, docker volume mount - Remove hardcoded 6-device keyboard fallback - Remove legacy step-based keyboard rendering (installationStep etc.) - Add "config not configured" message when Remnawave config is missing - Update admin UI: "clear config" disables guide mode instead of reverting
311 lines
12 KiB
Python
311 lines
12 KiB
Python
from aiogram import types
|
|
from aiogram.types import InaccessibleMessage, InlineKeyboardButton, InlineKeyboardMarkup
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.config import settings
|
|
from app.database.models import User
|
|
from app.keyboards.inline import (
|
|
get_device_selection_keyboard,
|
|
get_happ_cryptolink_keyboard,
|
|
get_happ_download_button_row,
|
|
)
|
|
from app.localization.texts import get_texts
|
|
from app.utils.subscription_utils import (
|
|
convert_subscription_link_to_happ_scheme,
|
|
get_display_subscription_link,
|
|
get_happ_cryptolink_redirect_link,
|
|
)
|
|
|
|
from .common import get_platforms_list, load_app_config_async, logger
|
|
|
|
|
|
async def handle_connect_subscription(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
|
# Проверяем, доступно ли сообщение для редактирования
|
|
if isinstance(callback.message, InaccessibleMessage):
|
|
await callback.answer()
|
|
return
|
|
|
|
texts = get_texts(db_user.language)
|
|
subscription = db_user.subscription
|
|
subscription_link = get_display_subscription_link(subscription)
|
|
hide_subscription_link = settings.should_hide_subscription_link()
|
|
|
|
if not subscription_link:
|
|
await callback.answer(
|
|
texts.t(
|
|
'SUBSCRIPTION_NO_ACTIVE_LINK',
|
|
'⚠ У вас нет активной подписки или ссылка еще генерируется',
|
|
),
|
|
show_alert=True,
|
|
)
|
|
return
|
|
|
|
connect_mode = settings.CONNECT_BUTTON_MODE
|
|
|
|
if connect_mode == 'miniapp_subscription':
|
|
keyboard = InlineKeyboardMarkup(
|
|
inline_keyboard=[
|
|
[
|
|
InlineKeyboardButton(
|
|
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
|
web_app=types.WebAppInfo(url=subscription_link),
|
|
)
|
|
],
|
|
[InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')],
|
|
]
|
|
)
|
|
|
|
await callback.message.edit_text(
|
|
texts.t(
|
|
'SUBSCRIPTION_CONNECT_MINIAPP_MESSAGE',
|
|
"""📱 <b>Подключить подписку</b>
|
|
|
|
🚀 Нажмите кнопку ниже, чтобы открыть подписку в мини-приложении Telegram:""",
|
|
),
|
|
reply_markup=keyboard,
|
|
parse_mode='HTML',
|
|
)
|
|
|
|
elif connect_mode == 'miniapp_custom':
|
|
if not settings.MINIAPP_CUSTOM_URL:
|
|
await callback.answer(
|
|
texts.t(
|
|
'CUSTOM_MINIAPP_URL_NOT_SET',
|
|
'⚠ Кастомная ссылка для мини-приложения не настроена',
|
|
),
|
|
show_alert=True,
|
|
)
|
|
return
|
|
|
|
keyboard = InlineKeyboardMarkup(
|
|
inline_keyboard=[
|
|
[
|
|
InlineKeyboardButton(
|
|
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
|
web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL),
|
|
)
|
|
],
|
|
[InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')],
|
|
]
|
|
)
|
|
|
|
await callback.message.edit_text(
|
|
texts.t(
|
|
'SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE',
|
|
"""🚀 <b>Подключить подписку</b>
|
|
|
|
📱 Нажмите кнопку ниже, чтобы открыть приложение:""",
|
|
),
|
|
reply_markup=keyboard,
|
|
parse_mode='HTML',
|
|
)
|
|
|
|
elif connect_mode == 'link':
|
|
rows = [[InlineKeyboardButton(text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'), url=subscription_link)]]
|
|
happ_row = get_happ_download_button_row(texts)
|
|
if happ_row:
|
|
rows.append(happ_row)
|
|
rows.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')])
|
|
|
|
keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
|
|
|
|
await callback.message.edit_text(
|
|
texts.t(
|
|
'SUBSCRIPTION_CONNECT_LINK_MESSAGE',
|
|
"""🚀 <b>Подключить подписку</b>",
|
|
|
|
🔗 Нажмите кнопку ниже, чтобы открыть ссылку подписки:""",
|
|
),
|
|
reply_markup=keyboard,
|
|
parse_mode='HTML',
|
|
)
|
|
elif connect_mode == 'happ_cryptolink':
|
|
rows = [
|
|
[
|
|
InlineKeyboardButton(
|
|
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
|
callback_data='open_subscription_link',
|
|
)
|
|
]
|
|
]
|
|
happ_row = get_happ_download_button_row(texts)
|
|
if happ_row:
|
|
rows.append(happ_row)
|
|
rows.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')])
|
|
|
|
keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
|
|
|
|
await callback.message.edit_text(
|
|
texts.t(
|
|
'SUBSCRIPTION_CONNECT_LINK_MESSAGE',
|
|
"""🚀 <b>Подключить подписку</b>",
|
|
|
|
🔗 Нажмите кнопку ниже, чтобы открыть ссылку подписки:""",
|
|
),
|
|
reply_markup=keyboard,
|
|
parse_mode='HTML',
|
|
)
|
|
else:
|
|
# Guide mode: load config and build dynamic platform keyboard
|
|
platforms = None
|
|
try:
|
|
config = await load_app_config_async()
|
|
if config:
|
|
platforms = get_platforms_list(config) or None
|
|
except Exception as e:
|
|
logger.warning('Failed to load platforms for guide mode', error=e)
|
|
|
|
if not platforms:
|
|
await callback.message.edit_text(
|
|
texts.t(
|
|
'GUIDE_CONFIG_NOT_SET',
|
|
'⚠️ <b>Конфигурация не настроена</b>\n\n'
|
|
'Администратор ещё не настроил конфигурацию приложений.\n'
|
|
'Обратитесь к администратору.',
|
|
),
|
|
reply_markup=InlineKeyboardMarkup(
|
|
inline_keyboard=[
|
|
[InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')],
|
|
]
|
|
),
|
|
parse_mode='HTML',
|
|
)
|
|
await callback.answer()
|
|
return
|
|
|
|
if hide_subscription_link:
|
|
device_text = texts.t(
|
|
'SUBSCRIPTION_CONNECT_DEVICE_MESSAGE_HIDDEN',
|
|
"""📱 <b>Подключить подписку</b>
|
|
|
|
ℹ️ Ссылка подписки доступна по кнопкам ниже или в разделе "Моя подписка".
|
|
|
|
💡 <b>Выберите ваше устройство</b> для получения подробной инструкции по настройке:""",
|
|
)
|
|
else:
|
|
device_text = texts.t(
|
|
'SUBSCRIPTION_CONNECT_DEVICE_MESSAGE',
|
|
"""📱 <b>Подключить подписку</b>
|
|
|
|
🔗 <b>Ссылка подписки:</b>
|
|
<code>{subscription_url}</code>
|
|
|
|
💡 <b>Выберите ваше устройство</b> для получения подробной инструкции по настройке:""",
|
|
).format(subscription_url=subscription_link)
|
|
|
|
await callback.message.edit_text(
|
|
device_text,
|
|
reply_markup=get_device_selection_keyboard(db_user.language, platforms=platforms),
|
|
parse_mode='HTML',
|
|
)
|
|
|
|
await callback.answer()
|
|
|
|
|
|
async def handle_open_subscription_link(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
|
texts = get_texts(db_user.language)
|
|
subscription = db_user.subscription
|
|
subscription_link = get_display_subscription_link(subscription)
|
|
|
|
if not subscription_link:
|
|
await callback.answer(
|
|
texts.t('SUBSCRIPTION_LINK_UNAVAILABLE', '❌ Ссылка подписки недоступна'),
|
|
show_alert=True,
|
|
)
|
|
return
|
|
|
|
if settings.is_happ_cryptolink_mode():
|
|
redirect_link = get_happ_cryptolink_redirect_link(subscription_link)
|
|
happ_scheme_link = convert_subscription_link_to_happ_scheme(subscription_link)
|
|
happ_message = (
|
|
texts.t(
|
|
'SUBSCRIPTION_HAPP_OPEN_TITLE',
|
|
'🔗 <b>Подключение через Happ</b>',
|
|
)
|
|
+ '\n\n'
|
|
+ texts.t(
|
|
'SUBSCRIPTION_HAPP_OPEN_LINK',
|
|
'<a href="{subscription_link}">🔓 Открыть ссылку в Happ</a>',
|
|
).format(subscription_link=happ_scheme_link)
|
|
+ '\n\n'
|
|
+ texts.t(
|
|
'SUBSCRIPTION_HAPP_OPEN_HINT',
|
|
'💡 Если ссылка не открывается автоматически, скопируйте её вручную:',
|
|
)
|
|
)
|
|
|
|
if redirect_link:
|
|
happ_message += '\n\n' + texts.t(
|
|
'SUBSCRIPTION_HAPP_OPEN_BUTTON_HINT',
|
|
'▶️ Нажмите кнопку "Подключиться" ниже, чтобы открыть Happ и добавить подписку автоматически.',
|
|
)
|
|
|
|
happ_message += '\n\n' + texts.t(
|
|
'SUBSCRIPTION_HAPP_CRYPTOLINK_BLOCK',
|
|
'<blockquote expandable><code>{crypto_link}</code></blockquote>',
|
|
).format(crypto_link=subscription_link)
|
|
|
|
keyboard = get_happ_cryptolink_keyboard(
|
|
subscription_link,
|
|
db_user.language,
|
|
redirect_link=redirect_link,
|
|
)
|
|
|
|
await callback.message.answer(
|
|
happ_message,
|
|
parse_mode='HTML',
|
|
disable_web_page_preview=True,
|
|
reply_markup=keyboard,
|
|
)
|
|
await callback.answer()
|
|
return
|
|
|
|
link_text = (
|
|
texts.t('SUBSCRIPTION_DEVICE_LINK_TITLE', '🔗 <b>Ссылка подписки:</b>')
|
|
+ '\n\n'
|
|
+ f'<code>{subscription_link}</code>\n\n'
|
|
+ texts.t('SUBSCRIPTION_LINK_USAGE_TITLE', '📱 <b>Как использовать:</b>')
|
|
+ '\n'
|
|
+ '\n'.join(
|
|
[
|
|
texts.t(
|
|
'SUBSCRIPTION_LINK_STEP1',
|
|
'1. Нажмите на ссылку выше чтобы её скопировать',
|
|
),
|
|
texts.t(
|
|
'SUBSCRIPTION_LINK_STEP2',
|
|
'2. Откройте ваше VPN приложение',
|
|
),
|
|
texts.t(
|
|
'SUBSCRIPTION_LINK_STEP3',
|
|
'3. Найдите функцию "Добавить подписку" или "Import"',
|
|
),
|
|
texts.t(
|
|
'SUBSCRIPTION_LINK_STEP4',
|
|
'4. Вставьте скопированную ссылку',
|
|
),
|
|
]
|
|
)
|
|
+ '\n\n'
|
|
+ texts.t(
|
|
'SUBSCRIPTION_LINK_HINT',
|
|
'💡 Если ссылка не скопировалась, выделите её вручную и скопируйте.',
|
|
)
|
|
)
|
|
|
|
await callback.message.edit_text(
|
|
link_text,
|
|
reply_markup=InlineKeyboardMarkup(
|
|
inline_keyboard=[
|
|
[
|
|
InlineKeyboardButton(
|
|
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'), callback_data='subscription_connect'
|
|
)
|
|
],
|
|
[InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')],
|
|
]
|
|
),
|
|
parse_mode='HTML',
|
|
)
|
|
await callback.answer()
|