mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-22 12:21:26 +00:00
Remove all modem purchase/management code: - Delete modem handler, service, and tests - Remove modem button from keyboards and admin panel - Remove modem pricing from calculations - Remove modem REST API endpoint and schemas - Remove modem decorator, config settings, and notification formatting - Keep DB column and migration for backwards compatibility
2951 lines
106 KiB
Python
2951 lines
106 KiB
Python
import logging
|
||
from datetime import UTC, datetime
|
||
|
||
from aiogram import types
|
||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.config import PERIOD_PRICES, settings
|
||
from app.database.models import User
|
||
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
|
||
from app.utils.price_display import PriceInfo, format_price_button
|
||
from app.utils.pricing_utils import (
|
||
apply_percentage_discount,
|
||
format_period_description,
|
||
)
|
||
from app.utils.subscription_utils import (
|
||
get_display_subscription_link,
|
||
get_happ_cryptolink_redirect_link,
|
||
)
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
async def get_main_menu_keyboard_async(
|
||
db: AsyncSession,
|
||
language: str = DEFAULT_LANGUAGE,
|
||
is_admin: bool = False,
|
||
has_had_paid_subscription: bool = False,
|
||
has_active_subscription: bool = False,
|
||
subscription_is_active: bool = False,
|
||
balance_kopeks: int = 0,
|
||
subscription=None,
|
||
show_resume_checkout: bool = False,
|
||
has_saved_cart: bool = False,
|
||
*,
|
||
is_moderator: bool = False,
|
||
custom_buttons: list[InlineKeyboardButton] | None = None,
|
||
user=None, # Добавляем параметр пользователя для получения данных
|
||
) -> InlineKeyboardMarkup:
|
||
"""
|
||
Асинхронная версия get_main_menu_keyboard с поддержкой конструктора меню.
|
||
|
||
Если MENU_LAYOUT_ENABLED=True, использует конфигурацию из БД.
|
||
Иначе делегирует в синхронную версию.
|
||
"""
|
||
if settings.MENU_LAYOUT_ENABLED:
|
||
from app.services.menu_layout_service import MenuContext, MenuLayoutService
|
||
|
||
# Получаем данные для плейсхолдеров
|
||
subscription_days_left = 0
|
||
traffic_used_gb = 0.0
|
||
traffic_left_gb = 0.0
|
||
referral_count = 0
|
||
referral_earnings_kopeks = 0
|
||
registration_days = 0
|
||
promo_group_id = None
|
||
has_autopay = False
|
||
username = ''
|
||
|
||
# Заполняем данными из подписки
|
||
if subscription:
|
||
# Дни до окончания подписки
|
||
if hasattr(subscription, 'days_left'):
|
||
# Используем свойство из модели, которое правильно вычисляет дни в UTC
|
||
subscription_days_left = subscription.days_left
|
||
elif hasattr(subscription, 'end_date') and subscription.end_date:
|
||
# Fallback: вычисляем вручную, используя UTC
|
||
now_utc = datetime.now(UTC).replace(tzinfo=None)
|
||
days_left = (subscription.end_date - now_utc).days
|
||
subscription_days_left = max(0, days_left)
|
||
|
||
# Трафик
|
||
if hasattr(subscription, 'traffic_used_gb'):
|
||
traffic_used_gb = subscription.traffic_used_gb or 0.0
|
||
|
||
if hasattr(subscription, 'traffic_limit_gb') and subscription.traffic_limit_gb:
|
||
traffic_left_gb = max(0, subscription.traffic_limit_gb - (subscription.traffic_used_gb or 0))
|
||
|
||
# Автоплатеж
|
||
if hasattr(subscription, 'autopay_enabled'):
|
||
has_autopay = subscription.autopay_enabled
|
||
|
||
# Получаем данные пользователя
|
||
if user:
|
||
# Имя пользователя
|
||
if hasattr(user, 'username') and user.username:
|
||
username = user.username
|
||
elif hasattr(user, 'first_name') and user.first_name:
|
||
username = user.first_name
|
||
|
||
# Дни с регистрации
|
||
if hasattr(user, 'created_at') and user.created_at:
|
||
now_utc = datetime.now(UTC).replace(tzinfo=None)
|
||
registration_days = (now_utc - user.created_at).days
|
||
|
||
# ID промо-группы
|
||
if hasattr(user, 'promo_group_id'):
|
||
promo_group_id = user.promo_group_id
|
||
|
||
# Получаем данные о рефералах из БД (если нужно)
|
||
try:
|
||
from app.database.crud.referral import get_user_referral_stats
|
||
|
||
if user and hasattr(user, 'id'):
|
||
referral_data = await get_user_referral_stats(db, user.id)
|
||
if referral_data:
|
||
referral_count = referral_data.get('invited_count', 0)
|
||
referral_earnings_kopeks = referral_data.get('total_earned_kopeks', 0)
|
||
except Exception as e:
|
||
logger.error(f'Error getting referral data: {e}')
|
||
|
||
context = MenuContext(
|
||
language=language,
|
||
is_admin=is_admin,
|
||
is_moderator=is_moderator,
|
||
has_active_subscription=has_active_subscription,
|
||
subscription_is_active=subscription_is_active,
|
||
has_had_paid_subscription=has_had_paid_subscription,
|
||
balance_kopeks=balance_kopeks,
|
||
subscription=subscription,
|
||
show_resume_checkout=show_resume_checkout,
|
||
has_saved_cart=has_saved_cart,
|
||
custom_buttons=custom_buttons or [],
|
||
# Добавляем данные для плейсхолдеров
|
||
username=username,
|
||
subscription_days=subscription_days_left,
|
||
traffic_used_gb=traffic_used_gb,
|
||
traffic_left_gb=traffic_left_gb,
|
||
referral_count=referral_count,
|
||
referral_earnings_kopeks=referral_earnings_kopeks,
|
||
registration_days=registration_days,
|
||
promo_group_id=promo_group_id,
|
||
has_autopay=has_autopay,
|
||
)
|
||
|
||
return await MenuLayoutService.build_keyboard(db, context)
|
||
|
||
# Fallback на синхронную версию
|
||
return get_main_menu_keyboard(
|
||
language=language,
|
||
is_admin=is_admin,
|
||
has_had_paid_subscription=has_had_paid_subscription,
|
||
has_active_subscription=has_active_subscription,
|
||
subscription_is_active=subscription_is_active,
|
||
balance_kopeks=balance_kopeks,
|
||
subscription=subscription,
|
||
show_resume_checkout=show_resume_checkout,
|
||
has_saved_cart=has_saved_cart,
|
||
is_moderator=is_moderator,
|
||
custom_buttons=custom_buttons,
|
||
)
|
||
|
||
|
||
def _get_localized_value(values, language: str, default_language: str = 'en') -> str:
|
||
if not isinstance(values, dict):
|
||
return ''
|
||
|
||
candidates = []
|
||
normalized_language = (language or '').strip().lower()
|
||
|
||
if normalized_language:
|
||
candidates.append(normalized_language)
|
||
if '-' in normalized_language:
|
||
candidates.append(normalized_language.split('-')[0])
|
||
|
||
default_language = (default_language or '').strip().lower()
|
||
if default_language and default_language not in candidates:
|
||
candidates.append(default_language)
|
||
|
||
for candidate in candidates:
|
||
if not candidate:
|
||
continue
|
||
value = values.get(candidate)
|
||
if isinstance(value, str) and value.strip():
|
||
return value
|
||
|
||
for value in values.values():
|
||
if isinstance(value, str) and value.strip():
|
||
return value
|
||
|
||
return ''
|
||
|
||
|
||
def _build_additional_buttons(additional_section, language: str) -> list[InlineKeyboardButton]:
|
||
if not isinstance(additional_section, dict):
|
||
return []
|
||
|
||
buttons = additional_section.get('buttons')
|
||
if not isinstance(buttons, list):
|
||
return []
|
||
|
||
localized_buttons: list[InlineKeyboardButton] = []
|
||
|
||
for button in buttons:
|
||
if not isinstance(button, dict):
|
||
continue
|
||
|
||
button_text = _get_localized_value(button.get('buttonText'), language)
|
||
button_link = button.get('buttonLink')
|
||
|
||
if not button_text or not button_link:
|
||
continue
|
||
|
||
localized_buttons.append(InlineKeyboardButton(text=button_text, url=button_link))
|
||
|
||
return localized_buttons
|
||
|
||
|
||
_LANGUAGE_DISPLAY_NAMES = {
|
||
'ru': '🇷🇺 Русский',
|
||
'ru-ru': '🇷🇺 Русский',
|
||
'en': '🇬🇧 English',
|
||
'en-us': '🇺🇸 English',
|
||
'en-gb': '🇬🇧 English',
|
||
'ua': '🇺🇦 Українська',
|
||
'uk': '🇺🇦 Українська',
|
||
'uk-ua': '🇺🇦 Українська',
|
||
'kk': '🇰🇿 Қазақша',
|
||
'kk-kz': '🇰🇿 Қазақша',
|
||
'kz': '🇰🇿 Қазақша',
|
||
'uz': '🇺🇿 Oʻzbekcha',
|
||
'uz-uz': '🇺🇿 Oʻzbekcha',
|
||
'tr': '🇹🇷 Türkçe',
|
||
'tr-tr': '🇹🇷 Türkçe',
|
||
'pl': '🇵🇱 Polski',
|
||
'pl-pl': '🇵🇱 Polski',
|
||
'de': '🇩🇪 Deutsch',
|
||
'de-de': '🇩🇪 Deutsch',
|
||
'fr': '🇫🇷 Français',
|
||
'fr-fr': '🇫🇷 Français',
|
||
'es': '🇪🇸 Español',
|
||
'es-es': '🇪🇸 Español',
|
||
'it': '🇮🇹 Italiano',
|
||
'it-it': '🇮🇹 Italiano',
|
||
'pt': '🇵🇹 Português',
|
||
'pt-pt': '🇵🇹 Português',
|
||
'pt-br': '🇧🇷 Português',
|
||
'zh': '🇨🇳 中文',
|
||
'zh-cn': '🇨🇳 中文 (简体)',
|
||
'zh-hans': '🇨🇳 中文 (简体)',
|
||
'zh-tw': '🇹🇼 中文 (繁體)',
|
||
'zh-hant': '🇹🇼 中文 (繁體)',
|
||
'vi': '🇻🇳 Tiếng Việt',
|
||
'vi-vn': '🇻🇳 Tiếng Việt',
|
||
'fa': '🇮🇷 فارسی',
|
||
'fa-ir': '🇮🇷 فارسی',
|
||
}
|
||
|
||
|
||
def get_rules_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(text=texts.RULES_ACCEPT, callback_data='rules_accept'),
|
||
InlineKeyboardButton(text=texts.RULES_DECLINE, callback_data='rules_decline'),
|
||
]
|
||
]
|
||
)
|
||
|
||
|
||
def get_privacy_policy_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(text=texts.PRIVACY_POLICY_ACCEPT, callback_data='privacy_policy_accept'),
|
||
InlineKeyboardButton(text=texts.PRIVACY_POLICY_DECLINE, callback_data='privacy_policy_decline'),
|
||
]
|
||
]
|
||
)
|
||
|
||
|
||
def get_channel_sub_keyboard(
|
||
channel_link: str | None,
|
||
language: str = DEFAULT_LANGUAGE,
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
|
||
buttons: list[list[InlineKeyboardButton]] = []
|
||
|
||
if channel_link:
|
||
buttons.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CHANNEL_SUBSCRIBE_BUTTON', '🔗 Подписаться'),
|
||
url=channel_link,
|
||
)
|
||
]
|
||
)
|
||
|
||
buttons.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CHANNEL_CHECK_BUTTON', '✅ Я подписался'),
|
||
callback_data='sub_channel_check',
|
||
)
|
||
]
|
||
)
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
|
||
def get_post_registration_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('POST_REGISTRATION_TRIAL_BUTTON', '🚀 Подключиться бесплатно 🚀'),
|
||
callback_data='trial_activate',
|
||
)
|
||
],
|
||
[InlineKeyboardButton(text=texts.t('SKIP_BUTTON', 'Пропустить ➡️'), callback_data='back_to_menu')],
|
||
]
|
||
)
|
||
|
||
|
||
def get_language_selection_keyboard(
|
||
current_language: str | None = None,
|
||
*,
|
||
include_back: bool = False,
|
||
language: str = DEFAULT_LANGUAGE,
|
||
) -> InlineKeyboardMarkup:
|
||
available_languages = settings.get_available_languages()
|
||
|
||
buttons: list[list[InlineKeyboardButton]] = []
|
||
row: list[InlineKeyboardButton] = []
|
||
|
||
normalized_current = (current_language or '').lower()
|
||
|
||
for index, lang_code in enumerate(available_languages, start=1):
|
||
normalized_code = lang_code.lower()
|
||
display_name = _LANGUAGE_DISPLAY_NAMES.get(
|
||
normalized_code,
|
||
normalized_code.upper(),
|
||
)
|
||
|
||
prefix = '✅ ' if normalized_code == normalized_current and normalized_current else ''
|
||
|
||
row.append(
|
||
InlineKeyboardButton(
|
||
text=f'{prefix}{display_name}',
|
||
callback_data=f'language_select:{normalized_code}',
|
||
)
|
||
)
|
||
|
||
if len(row) == 2:
|
||
buttons.append(row)
|
||
row = []
|
||
|
||
if row:
|
||
buttons.append(row)
|
||
|
||
if include_back:
|
||
texts = get_texts(language)
|
||
buttons.append([InlineKeyboardButton(text=texts.BACK, callback_data='back_to_menu')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
|
||
def _build_text_main_menu_keyboard(
|
||
language: str,
|
||
texts,
|
||
*,
|
||
is_admin: bool,
|
||
is_moderator: bool,
|
||
) -> InlineKeyboardMarkup:
|
||
profile_text = texts.t('MENU_PROFILE', '👤 Личный кабинет')
|
||
miniapp_url = settings.get_main_menu_miniapp_url()
|
||
|
||
if miniapp_url:
|
||
profile_button = InlineKeyboardButton(
|
||
text=profile_text,
|
||
web_app=types.WebAppInfo(url=miniapp_url),
|
||
)
|
||
else:
|
||
profile_button = InlineKeyboardButton(
|
||
text=profile_text,
|
||
callback_data='menu_profile_unavailable',
|
||
)
|
||
|
||
keyboard_rows: list[list[InlineKeyboardButton]] = [[profile_button]]
|
||
|
||
if settings.is_language_selection_enabled():
|
||
keyboard_rows.append([InlineKeyboardButton(text=texts.MENU_LANGUAGE, callback_data='menu_language')])
|
||
|
||
support_enabled = False
|
||
try:
|
||
from app.services.support_settings_service import SupportSettingsService
|
||
|
||
support_enabled = SupportSettingsService.is_support_menu_enabled()
|
||
except Exception:
|
||
support_enabled = settings.SUPPORT_MENU_ENABLED
|
||
|
||
if support_enabled:
|
||
keyboard_rows.append([InlineKeyboardButton(text=texts.MENU_SUPPORT, callback_data='menu_support')])
|
||
|
||
if is_admin:
|
||
keyboard_rows.append([InlineKeyboardButton(text=texts.MENU_ADMIN, callback_data='admin_panel')])
|
||
elif is_moderator:
|
||
keyboard_rows.append([InlineKeyboardButton(text='🧑⚖️ Модерация', callback_data='moderator_panel')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard_rows)
|
||
|
||
|
||
def get_main_menu_keyboard(
|
||
language: str = DEFAULT_LANGUAGE,
|
||
is_admin: bool = False,
|
||
has_had_paid_subscription: bool = False,
|
||
has_active_subscription: bool = False,
|
||
subscription_is_active: bool = False,
|
||
balance_kopeks: int = 0,
|
||
subscription=None,
|
||
show_resume_checkout: bool = False,
|
||
has_saved_cart: bool = False, # Новый параметр для отображения уведомления о сохраненной корзине
|
||
*,
|
||
is_moderator: bool = False,
|
||
custom_buttons: list[InlineKeyboardButton] | None = None,
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
|
||
if settings.is_text_main_menu_mode():
|
||
return _build_text_main_menu_keyboard(
|
||
language,
|
||
texts,
|
||
is_admin=is_admin,
|
||
is_moderator=is_moderator,
|
||
)
|
||
|
||
if settings.DEBUG:
|
||
print(
|
||
f'DEBUG KEYBOARD: language={language}, is_admin={is_admin}, has_had_paid={has_had_paid_subscription}, has_active={has_active_subscription}, sub_active={subscription_is_active}, balance={balance_kopeks}'
|
||
)
|
||
|
||
safe_balance = balance_kopeks or 0
|
||
if hasattr(texts, 'BALANCE_BUTTON') and safe_balance > 0:
|
||
balance_button_text = texts.BALANCE_BUTTON.format(balance=texts.format_price(safe_balance))
|
||
else:
|
||
balance_button_text = texts.t(
|
||
'BALANCE_BUTTON_DEFAULT',
|
||
'💰 Баланс: {balance}',
|
||
).format(balance=texts.format_price(safe_balance))
|
||
|
||
keyboard: list[list[InlineKeyboardButton]] = []
|
||
paired_buttons: list[InlineKeyboardButton] = []
|
||
|
||
if has_active_subscription and subscription_is_active:
|
||
connect_mode = settings.CONNECT_BUTTON_MODE
|
||
subscription_link = get_display_subscription_link(subscription)
|
||
|
||
def _fallback_connect_button() -> InlineKeyboardButton:
|
||
return InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
||
callback_data='subscription_connect',
|
||
)
|
||
|
||
if connect_mode == 'miniapp_subscription':
|
||
if subscription_link:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
||
web_app=types.WebAppInfo(url=subscription_link),
|
||
)
|
||
]
|
||
)
|
||
else:
|
||
keyboard.append([_fallback_connect_button()])
|
||
elif connect_mode == 'miniapp_custom':
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
||
web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL),
|
||
)
|
||
]
|
||
)
|
||
elif connect_mode == 'link':
|
||
if subscription_link:
|
||
keyboard.append(
|
||
[InlineKeyboardButton(text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'), url=subscription_link)]
|
||
)
|
||
else:
|
||
keyboard.append([_fallback_connect_button()])
|
||
elif connect_mode == 'happ_cryptolink':
|
||
if subscription_link:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
||
callback_data='open_subscription_link',
|
||
)
|
||
]
|
||
)
|
||
else:
|
||
keyboard.append([_fallback_connect_button()])
|
||
else:
|
||
keyboard.append([_fallback_connect_button()])
|
||
|
||
happ_row = get_happ_download_button_row(texts)
|
||
if happ_row:
|
||
keyboard.append(happ_row)
|
||
paired_buttons.append(InlineKeyboardButton(text=texts.MENU_SUBSCRIPTION, callback_data='menu_subscription'))
|
||
|
||
# Добавляем кнопку докупки трафика для лимитированных подписок
|
||
# В режиме тарифов проверяем tariff_id (детальная проверка в хендлере)
|
||
# В классическом режиме проверяем глобальные настройки
|
||
show_traffic_topup = False
|
||
if subscription and not subscription.is_trial and (subscription.traffic_limit_gb or 0) > 0:
|
||
if settings.is_tariffs_mode() and getattr(subscription, 'tariff_id', None):
|
||
# Режим тарифов - показываем кнопку, проверка настроек тарифа в хендлере
|
||
show_traffic_topup = settings.BUY_TRAFFIC_BUTTON_VISIBLE
|
||
elif settings.is_traffic_topup_enabled() and not settings.is_traffic_topup_blocked():
|
||
# Классический режим - проверяем глобальные настройки
|
||
show_traffic_topup = settings.BUY_TRAFFIC_BUTTON_VISIBLE
|
||
|
||
if show_traffic_topup:
|
||
paired_buttons.append(
|
||
InlineKeyboardButton(
|
||
text=texts.t('BUY_TRAFFIC_BUTTON', '📈 Докупить трафик'), callback_data='buy_traffic'
|
||
)
|
||
)
|
||
|
||
keyboard.append([InlineKeyboardButton(text=balance_button_text, callback_data='menu_balance')])
|
||
|
||
show_trial = not has_had_paid_subscription and not has_active_subscription
|
||
|
||
show_buy = not has_active_subscription or not subscription_is_active
|
||
current_subscription = subscription
|
||
bool(
|
||
current_subscription
|
||
and not getattr(current_subscription, 'is_trial', False)
|
||
and getattr(current_subscription, 'is_active', False)
|
||
)
|
||
simple_purchase_button = None
|
||
if settings.SIMPLE_SUBSCRIPTION_ENABLED:
|
||
simple_purchase_button = InlineKeyboardButton(
|
||
text=texts.MENU_SIMPLE_SUBSCRIPTION,
|
||
callback_data='simple_subscription_purchase',
|
||
)
|
||
|
||
subscription_buttons: list[InlineKeyboardButton] = []
|
||
|
||
if show_trial:
|
||
subscription_buttons.append(InlineKeyboardButton(text=texts.MENU_TRIAL, callback_data='menu_trial'))
|
||
|
||
if show_buy:
|
||
subscription_buttons.append(InlineKeyboardButton(text=texts.MENU_BUY_SUBSCRIPTION, callback_data='menu_buy'))
|
||
|
||
if subscription_buttons:
|
||
paired_buttons.extend(subscription_buttons)
|
||
if simple_purchase_button:
|
||
paired_buttons.append(simple_purchase_button)
|
||
|
||
if show_resume_checkout or has_saved_cart:
|
||
resume_callback = 'return_to_saved_cart' if has_saved_cart else 'subscription_resume_checkout'
|
||
paired_buttons.append(
|
||
InlineKeyboardButton(
|
||
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
|
||
callback_data=resume_callback,
|
||
)
|
||
)
|
||
|
||
if custom_buttons:
|
||
for button in custom_buttons:
|
||
if isinstance(button, InlineKeyboardButton):
|
||
paired_buttons.append(button)
|
||
|
||
# Добавляем кнопки промокода и рефералов, учитывая настройки
|
||
paired_buttons.append(InlineKeyboardButton(text=texts.MENU_PROMOCODE, callback_data='menu_promocode'))
|
||
|
||
# Добавляем кнопку рефералов, только если программа включена
|
||
if settings.is_referral_program_enabled():
|
||
paired_buttons.append(InlineKeyboardButton(text=texts.MENU_REFERRALS, callback_data='menu_referrals'))
|
||
|
||
# Добавляем кнопку конкурсов
|
||
if settings.CONTESTS_ENABLED and settings.CONTESTS_BUTTON_VISIBLE:
|
||
paired_buttons.append(
|
||
InlineKeyboardButton(text=texts.t('CONTESTS_BUTTON', '🎲 Конкурсы'), callback_data='contests_menu')
|
||
)
|
||
|
||
try:
|
||
from app.services.support_settings_service import SupportSettingsService
|
||
|
||
support_enabled = SupportSettingsService.is_support_menu_enabled()
|
||
except Exception:
|
||
support_enabled = settings.SUPPORT_MENU_ENABLED
|
||
|
||
if support_enabled:
|
||
paired_buttons.append(InlineKeyboardButton(text=texts.MENU_SUPPORT, callback_data='menu_support'))
|
||
|
||
# Добавляем кнопку активации
|
||
if settings.ACTIVATE_BUTTON_VISIBLE:
|
||
paired_buttons.append(InlineKeyboardButton(text=settings.ACTIVATE_BUTTON_TEXT, callback_data='activate_button'))
|
||
|
||
paired_buttons.append(
|
||
InlineKeyboardButton(
|
||
text=texts.t('MENU_INFO', 'ℹ️ Инфо'),
|
||
callback_data='menu_info',
|
||
)
|
||
)
|
||
|
||
if settings.is_language_selection_enabled():
|
||
paired_buttons.append(InlineKeyboardButton(text=texts.MENU_LANGUAGE, callback_data='menu_language'))
|
||
|
||
for i in range(0, len(paired_buttons), 2):
|
||
row = paired_buttons[i : i + 2]
|
||
keyboard.append(row)
|
||
|
||
if settings.DEBUG:
|
||
print(f'DEBUG KEYBOARD: is_admin={is_admin}, добавляем админ кнопку: {is_admin}')
|
||
|
||
if is_admin:
|
||
if settings.DEBUG:
|
||
print('DEBUG KEYBOARD: Админ кнопка ДОБАВЛЕНА!')
|
||
keyboard.append([InlineKeyboardButton(text=texts.MENU_ADMIN, callback_data='admin_panel')])
|
||
elif settings.DEBUG:
|
||
print('DEBUG KEYBOARD: Админ кнопка НЕ добавлена')
|
||
# Moderator access (limited support panel)
|
||
if (not is_admin) and is_moderator:
|
||
keyboard.append([InlineKeyboardButton(text='🧑⚖️ Модерация', callback_data='moderator_panel')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_info_menu_keyboard(
|
||
language: str = DEFAULT_LANGUAGE,
|
||
show_privacy_policy: bool = False,
|
||
show_public_offer: bool = False,
|
||
show_faq: bool = False,
|
||
show_promo_groups: bool = False,
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
|
||
buttons: list[list[InlineKeyboardButton]] = []
|
||
|
||
if show_faq:
|
||
buttons.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('MENU_FAQ', '❓ FAQ'),
|
||
callback_data='menu_faq',
|
||
)
|
||
]
|
||
)
|
||
|
||
if show_promo_groups:
|
||
buttons.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('MENU_PROMO_GROUPS_INFO', '🎯 Промогруппы'),
|
||
callback_data='menu_info_promo_groups',
|
||
)
|
||
]
|
||
)
|
||
|
||
if show_privacy_policy:
|
||
buttons.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('MENU_PRIVACY_POLICY', '🛡️ Политика конф.'),
|
||
callback_data='menu_privacy_policy',
|
||
)
|
||
]
|
||
)
|
||
|
||
if show_public_offer:
|
||
buttons.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('MENU_PUBLIC_OFFER', '📄 Оферта'),
|
||
callback_data='menu_public_offer',
|
||
)
|
||
]
|
||
)
|
||
|
||
buttons.append([InlineKeyboardButton(text=texts.MENU_RULES, callback_data='menu_rules')])
|
||
|
||
server_status_mode = settings.get_server_status_mode()
|
||
server_status_text = texts.t('MENU_SERVER_STATUS', '📊 Статус серверов')
|
||
|
||
if server_status_mode == 'external_link':
|
||
status_url = settings.get_server_status_external_url()
|
||
if status_url:
|
||
buttons.append([InlineKeyboardButton(text=server_status_text, url=status_url)])
|
||
elif server_status_mode == 'external_link_miniapp':
|
||
status_url = settings.get_server_status_external_url()
|
||
if status_url:
|
||
buttons.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=server_status_text,
|
||
web_app=types.WebAppInfo(url=status_url),
|
||
)
|
||
]
|
||
)
|
||
elif server_status_mode == 'xray':
|
||
buttons.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=server_status_text,
|
||
callback_data='menu_server_status',
|
||
)
|
||
]
|
||
)
|
||
|
||
buttons.append([InlineKeyboardButton(text=texts.BACK, callback_data='back_to_menu')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
|
||
def get_happ_download_button_row(texts) -> list[InlineKeyboardButton] | None:
|
||
if not settings.is_happ_download_button_enabled():
|
||
return None
|
||
|
||
return [
|
||
InlineKeyboardButton(
|
||
text=texts.t('HAPP_DOWNLOAD_BUTTON', '⬇️ Скачать Happ'), callback_data='subscription_happ_download'
|
||
)
|
||
]
|
||
|
||
|
||
def get_happ_cryptolink_keyboard(
|
||
subscription_link: str,
|
||
language: str = DEFAULT_LANGUAGE,
|
||
redirect_link: str | None = None,
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
final_redirect_link = redirect_link or get_happ_cryptolink_redirect_link(subscription_link)
|
||
|
||
buttons: list[list[InlineKeyboardButton]] = []
|
||
|
||
if final_redirect_link:
|
||
buttons.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
||
url=final_redirect_link,
|
||
)
|
||
]
|
||
)
|
||
|
||
buttons.extend(
|
||
[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('HAPP_PLATFORM_IOS', '🍎 iOS'),
|
||
callback_data='happ_download_ios',
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('HAPP_PLATFORM_ANDROID', '🤖 Android'),
|
||
callback_data='happ_download_android',
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('HAPP_PLATFORM_MACOS', '🖥️ Mac OS'),
|
||
callback_data='happ_download_macos',
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('HAPP_PLATFORM_WINDOWS', '💻 Windows'),
|
||
callback_data='happ_download_windows',
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('BACK_TO_MAIN_MENU_BUTTON', '⬅️ В главное меню'),
|
||
callback_data='back_to_menu',
|
||
)
|
||
],
|
||
]
|
||
)
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
|
||
def get_happ_download_platform_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
buttons = [
|
||
[InlineKeyboardButton(text=texts.t('HAPP_PLATFORM_IOS', '🍎 iOS'), callback_data='happ_download_ios')],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('HAPP_PLATFORM_ANDROID', '🤖 Android'), callback_data='happ_download_android'
|
||
)
|
||
],
|
||
[InlineKeyboardButton(text=texts.t('HAPP_PLATFORM_MACOS', '🖥️ Mac OS'), callback_data='happ_download_macos')],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('HAPP_PLATFORM_WINDOWS', '💻 Windows'), callback_data='happ_download_windows'
|
||
)
|
||
],
|
||
[InlineKeyboardButton(text=texts.BACK, callback_data='happ_download_close')],
|
||
]
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
|
||
def get_happ_download_link_keyboard(language: str, link: str) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
buttons = [
|
||
[InlineKeyboardButton(text=texts.t('HAPP_DOWNLOAD_OPEN_LINK', '🔗 Открыть ссылку'), url=link)],
|
||
[InlineKeyboardButton(text=texts.BACK, callback_data='happ_download_back')],
|
||
]
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
|
||
def get_back_keyboard(language: str = DEFAULT_LANGUAGE, callback_data: str = 'back_to_menu') -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
return InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text=texts.BACK, callback_data=callback_data)]])
|
||
|
||
|
||
def get_server_status_keyboard(
|
||
language: str,
|
||
current_page: int,
|
||
total_pages: int,
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
keyboard: list[list[InlineKeyboardButton]] = [
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('SERVER_STATUS_REFRESH', '🔄 Обновить'),
|
||
callback_data=f'server_status_page:{current_page}',
|
||
)
|
||
]
|
||
]
|
||
|
||
if total_pages > 1:
|
||
nav_row: list[InlineKeyboardButton] = []
|
||
|
||
if current_page > 1:
|
||
nav_row.append(
|
||
InlineKeyboardButton(
|
||
text=texts.t('SERVER_STATUS_PREV_PAGE', '⬅️ Назад'),
|
||
callback_data=f'server_status_page:{current_page - 1}',
|
||
)
|
||
)
|
||
|
||
if current_page < total_pages:
|
||
nav_row.append(
|
||
InlineKeyboardButton(
|
||
text=texts.t('SERVER_STATUS_NEXT_PAGE', 'Вперед ➡️'),
|
||
callback_data=f'server_status_page:{current_page + 1}',
|
||
)
|
||
)
|
||
|
||
if nav_row:
|
||
keyboard.append(nav_row)
|
||
|
||
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='back_to_menu')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_insufficient_balance_keyboard(
|
||
language: str = DEFAULT_LANGUAGE,
|
||
resume_callback: str | None = None,
|
||
amount_kopeks: int | None = None,
|
||
has_saved_cart: bool = False, # Новый параметр для указания наличия сохраненной корзины
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
keyboard = get_payment_methods_keyboard(amount_kopeks or 0, language)
|
||
|
||
back_row_index: int | None = None
|
||
|
||
if keyboard.inline_keyboard:
|
||
last_row = keyboard.inline_keyboard[-1]
|
||
if (
|
||
len(last_row) == 1
|
||
and isinstance(last_row[0], InlineKeyboardButton)
|
||
and last_row[0].callback_data in {'menu_balance', 'back_to_menu'}
|
||
):
|
||
keyboard.inline_keyboard[-1][0] = InlineKeyboardButton(
|
||
text=texts.t('PAYMENT_RETURN_HOME_BUTTON', '🏠 На главную'),
|
||
callback_data='back_to_menu',
|
||
)
|
||
back_row_index = len(keyboard.inline_keyboard) - 1
|
||
|
||
# Если есть сохраненная корзина, добавляем кнопку возврата к оформлению
|
||
if has_saved_cart:
|
||
return_row = [
|
||
InlineKeyboardButton(
|
||
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
|
||
callback_data='return_to_saved_cart',
|
||
)
|
||
]
|
||
insert_index = back_row_index if back_row_index is not None else len(keyboard.inline_keyboard)
|
||
keyboard.inline_keyboard.insert(insert_index, return_row)
|
||
elif resume_callback:
|
||
return_row = [
|
||
InlineKeyboardButton(
|
||
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
|
||
callback_data=resume_callback,
|
||
)
|
||
]
|
||
insert_index = back_row_index if back_row_index is not None else len(keyboard.inline_keyboard)
|
||
keyboard.inline_keyboard.insert(insert_index, return_row)
|
||
|
||
return keyboard
|
||
|
||
|
||
def get_subscription_keyboard(
|
||
language: str = DEFAULT_LANGUAGE, has_subscription: bool = False, is_trial: bool = False, subscription=None
|
||
) -> InlineKeyboardMarkup:
|
||
from app.config import settings
|
||
|
||
texts = get_texts(language)
|
||
keyboard = []
|
||
|
||
if has_subscription:
|
||
subscription_link = get_display_subscription_link(subscription) if subscription else None
|
||
if subscription_link:
|
||
connect_mode = settings.CONNECT_BUTTON_MODE
|
||
|
||
if connect_mode == 'miniapp_subscription':
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
||
web_app=types.WebAppInfo(url=subscription_link),
|
||
)
|
||
]
|
||
)
|
||
elif connect_mode == 'miniapp_custom':
|
||
if settings.MINIAPP_CUSTOM_URL:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
||
web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL),
|
||
)
|
||
]
|
||
)
|
||
else:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'), callback_data='subscription_connect'
|
||
)
|
||
]
|
||
)
|
||
elif connect_mode == 'link':
|
||
keyboard.append(
|
||
[InlineKeyboardButton(text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'), url=subscription_link)]
|
||
)
|
||
elif connect_mode == 'happ_cryptolink':
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
||
callback_data='open_subscription_link',
|
||
)
|
||
]
|
||
)
|
||
else:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'), callback_data='subscription_connect'
|
||
)
|
||
]
|
||
)
|
||
elif settings.CONNECT_BUTTON_MODE == 'miniapp_custom':
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
||
web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL),
|
||
)
|
||
]
|
||
)
|
||
else:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'), callback_data='subscription_connect'
|
||
)
|
||
]
|
||
)
|
||
|
||
happ_row = get_happ_download_button_row(texts)
|
||
if happ_row:
|
||
keyboard.append(happ_row)
|
||
|
||
if is_trial:
|
||
keyboard.append(
|
||
[InlineKeyboardButton(text=texts.MENU_BUY_SUBSCRIPTION, callback_data='subscription_upgrade')]
|
||
)
|
||
else:
|
||
# Проверяем, является ли тариф суточным
|
||
tariff = getattr(subscription, 'tariff', None) if subscription else None
|
||
is_daily_tariff = tariff and getattr(tariff, 'is_daily', False)
|
||
|
||
if is_daily_tariff:
|
||
# Для суточного тарифа показываем кнопку паузы/возобновления
|
||
is_paused = getattr(subscription, 'is_daily_paused', False)
|
||
if is_paused:
|
||
pause_text = texts.t('RESUME_DAILY_BUTTON', '▶️ Возобновить подписку')
|
||
else:
|
||
pause_text = texts.t('PAUSE_DAILY_BUTTON', '⏸️ Приостановить подписку')
|
||
keyboard.append(
|
||
[InlineKeyboardButton(text=pause_text, callback_data='toggle_daily_subscription_pause')]
|
||
)
|
||
else:
|
||
# Для обычного тарифа: [Продлить] [Автоплатеж]
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(text=texts.MENU_EXTEND_SUBSCRIPTION, callback_data='subscription_extend'),
|
||
InlineKeyboardButton(
|
||
text=texts.t('AUTOPAY_BUTTON', '💳 Автоплатеж'),
|
||
callback_data='subscription_autopay',
|
||
),
|
||
]
|
||
)
|
||
|
||
# Ряд: [Настройки] [Тариф] (если режим тарифов)
|
||
settings_row = [
|
||
InlineKeyboardButton(
|
||
text=texts.t('SUBSCRIPTION_SETTINGS_BUTTON', '⚙️ Настройки'),
|
||
callback_data='subscription_settings',
|
||
)
|
||
]
|
||
if settings.is_tariffs_mode() and subscription:
|
||
# Для суточных тарифов переходим на список тарифов, для обычных - мгновенное переключение
|
||
tariff_callback = 'tariff_switch' if is_daily_tariff else 'instant_switch'
|
||
settings_row.append(
|
||
InlineKeyboardButton(
|
||
text=texts.t('CHANGE_TARIFF_BUTTON', '📦 Тариф'), callback_data=tariff_callback
|
||
)
|
||
)
|
||
keyboard.append(settings_row)
|
||
|
||
# Кнопка докупки трафика для платных подписок
|
||
# В режиме тарифов проверяем tariff_id, в классическом - глобальные настройки
|
||
show_traffic_topup = False
|
||
if subscription and (subscription.traffic_limit_gb or 0) > 0:
|
||
if (settings.is_tariffs_mode() and getattr(subscription, 'tariff_id', None)) or (
|
||
settings.is_traffic_topup_enabled() and not settings.is_traffic_topup_blocked()
|
||
):
|
||
show_traffic_topup = True
|
||
|
||
if show_traffic_topup:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('BUY_TRAFFIC_BUTTON', '📈 Докупить трафик'), callback_data='buy_traffic'
|
||
)
|
||
]
|
||
)
|
||
|
||
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='back_to_menu')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_payment_methods_keyboard_with_cart(
|
||
language: str = 'ru',
|
||
amount_kopeks: int = 0,
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
keyboard = get_payment_methods_keyboard(amount_kopeks, language)
|
||
|
||
# Добавляем кнопку "Очистить корзину"
|
||
keyboard.inline_keyboard.append(
|
||
[InlineKeyboardButton(text='🗑️ Очистить корзину и вернуться', callback_data='clear_saved_cart')]
|
||
)
|
||
|
||
# Добавляем кнопку возврата к оформлению подписки
|
||
keyboard.inline_keyboard.insert(
|
||
-1,
|
||
[ # Вставляем перед кнопкой "назад"
|
||
InlineKeyboardButton(text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT, callback_data='return_to_saved_cart')
|
||
],
|
||
)
|
||
|
||
return keyboard
|
||
|
||
|
||
def get_subscription_confirm_keyboard_with_cart(language: str = 'ru') -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[InlineKeyboardButton(text='✅ Подтвердить покупку', callback_data='subscription_confirm')],
|
||
[InlineKeyboardButton(text='🗑️ Очистить корзину', callback_data='clear_saved_cart')],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.BACK,
|
||
callback_data='subscription_config_back', # Изменили на возврат к настройке
|
||
)
|
||
],
|
||
]
|
||
)
|
||
|
||
|
||
def get_insufficient_balance_keyboard_with_cart(
|
||
language: str = 'ru',
|
||
amount_kopeks: int = 0,
|
||
) -> InlineKeyboardMarkup:
|
||
# Используем обновленную версию с флагом has_saved_cart=True
|
||
keyboard = get_insufficient_balance_keyboard(
|
||
language,
|
||
amount_kopeks=amount_kopeks,
|
||
has_saved_cart=True,
|
||
)
|
||
|
||
# Добавляем кнопку очистки корзины в начало
|
||
keyboard.inline_keyboard.insert(
|
||
0,
|
||
[
|
||
InlineKeyboardButton(
|
||
text='🗑️ Очистить корзину и вернуться',
|
||
callback_data='clear_saved_cart',
|
||
)
|
||
],
|
||
)
|
||
|
||
return keyboard
|
||
|
||
|
||
def get_trial_keyboard(language: str = 'ru') -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('TRIAL_ACTIVATE_BUTTON', '🎁 Активировать'), callback_data='trial_activate'
|
||
),
|
||
InlineKeyboardButton(text=texts.BACK, callback_data='back_to_menu'),
|
||
]
|
||
]
|
||
)
|
||
|
||
|
||
def get_subscription_period_keyboard(
|
||
language: str = DEFAULT_LANGUAGE, user: User | None = None
|
||
) -> InlineKeyboardMarkup:
|
||
"""
|
||
Generate subscription period selection keyboard with personalized pricing.
|
||
|
||
Args:
|
||
language: User's language code
|
||
user: User object for personalized discounts (None = default discounts)
|
||
|
||
Returns:
|
||
InlineKeyboardMarkup with period buttons showing personalized prices
|
||
"""
|
||
from app.utils.price_display import calculate_user_price
|
||
|
||
texts = get_texts(language)
|
||
keyboard = []
|
||
|
||
available_periods = settings.get_available_subscription_periods()
|
||
|
||
for days in available_periods:
|
||
# Get base price for this period
|
||
base_price = PERIOD_PRICES.get(days, 0)
|
||
|
||
# Calculate personalized price with user's discounts
|
||
price_info = calculate_user_price(user, base_price, days, 'period')
|
||
|
||
# Format period description
|
||
period_display = format_period_description(days, language)
|
||
|
||
# Format button text with discount display
|
||
button_text = format_price_button(
|
||
period_label=period_display,
|
||
price_info=price_info,
|
||
format_price_func=texts.format_price,
|
||
emphasize=False,
|
||
add_exclamation=False,
|
||
)
|
||
|
||
keyboard.append([InlineKeyboardButton(text=button_text, callback_data=f'period_{days}')])
|
||
|
||
# Кнопка "Простая покупка" была убрана из выбора периода подписки
|
||
|
||
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='back_to_menu')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_traffic_packages_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
from app.config import settings
|
||
|
||
if settings.is_traffic_topup_blocked():
|
||
return get_back_keyboard(language)
|
||
|
||
logger.info(f"🔍 RAW CONFIG: '{settings.TRAFFIC_PACKAGES_CONFIG}'")
|
||
|
||
all_packages = settings.get_traffic_packages()
|
||
logger.info(f'🔍 ALL PACKAGES: {all_packages}')
|
||
|
||
enabled_packages = [pkg for pkg in all_packages if pkg['enabled']]
|
||
disabled_packages = [pkg for pkg in all_packages if not pkg['enabled']]
|
||
|
||
logger.info(f'🔍 ENABLED: {len(enabled_packages)} packages')
|
||
logger.info(f'🔍 DISABLED: {len(disabled_packages)} packages')
|
||
|
||
for pkg in disabled_packages:
|
||
logger.info(f'🔍 DISABLED PACKAGE: {pkg["gb"]}GB = {pkg["price"]} kopeks, enabled={pkg["enabled"]}')
|
||
|
||
texts = get_texts(language)
|
||
keyboard = []
|
||
|
||
traffic_packages = settings.get_traffic_packages()
|
||
|
||
for package in traffic_packages:
|
||
gb = package['gb']
|
||
package['price']
|
||
enabled = package['enabled']
|
||
|
||
if not enabled:
|
||
continue
|
||
|
||
if gb == 0:
|
||
text = f'♾️ Безлимит - {settings.format_price(package["price"])}'
|
||
else:
|
||
text = f'📊 {gb} ГБ - {settings.format_price(package["price"])}'
|
||
|
||
keyboard.append([InlineKeyboardButton(text=text, callback_data=f'traffic_{gb}')])
|
||
|
||
if not keyboard:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('TRAFFIC_PACKAGES_NOT_CONFIGURED', '⚠️ Пакеты трафика не настроены'),
|
||
callback_data='no_traffic_packages',
|
||
)
|
||
]
|
||
)
|
||
|
||
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='subscription_config_back')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_countries_keyboard(
|
||
countries: list[dict], selected: list[str], language: str = DEFAULT_LANGUAGE
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
keyboard = []
|
||
|
||
for country in countries:
|
||
if not country.get('is_available', True):
|
||
continue
|
||
|
||
emoji = '✅' if country['uuid'] in selected else '⚪'
|
||
|
||
if country['price_kopeks'] > 0:
|
||
price_text = f' (+{texts.format_price(country["price_kopeks"])})'
|
||
else:
|
||
price_text = ' (Бесплатно)'
|
||
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=f'{emoji} {country["name"]}{price_text}', callback_data=f'country_{country["uuid"]}'
|
||
)
|
||
]
|
||
)
|
||
|
||
if not keyboard:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('NO_SERVERS_AVAILABLE', '❌ Нет доступных серверов'), callback_data='no_servers'
|
||
)
|
||
]
|
||
)
|
||
|
||
keyboard.extend(
|
||
[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CONTINUE_BUTTON', '✅ Продолжить'), callback_data='countries_continue'
|
||
)
|
||
],
|
||
[InlineKeyboardButton(text=texts.BACK, callback_data='subscription_config_back')],
|
||
]
|
||
)
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_devices_keyboard(current: int, language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
keyboard = []
|
||
|
||
start_devices = settings.DEFAULT_DEVICE_LIMIT
|
||
max_devices = settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else 50
|
||
end_devices = min(max_devices + 1, start_devices + 10)
|
||
|
||
buttons = []
|
||
|
||
for devices in range(start_devices, end_devices):
|
||
price = max(0, devices - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
|
||
price_text = f' (+{texts.format_price(price)})' if price > 0 else ' (вкл.)'
|
||
emoji = '✅' if devices == current else '⚪'
|
||
|
||
button_text = f'{emoji} {devices}{price_text}'
|
||
|
||
buttons.append(InlineKeyboardButton(text=button_text, callback_data=f'devices_{devices}'))
|
||
|
||
for i in range(0, len(buttons), 2):
|
||
if i + 1 < len(buttons):
|
||
keyboard.append([buttons[i], buttons[i + 1]])
|
||
else:
|
||
keyboard.append([buttons[i]])
|
||
|
||
keyboard.extend(
|
||
[
|
||
[InlineKeyboardButton(text=texts.t('CONTINUE_BUTTON', '✅ Продолжить'), callback_data='devices_continue')],
|
||
[InlineKeyboardButton(text=texts.BACK, callback_data='subscription_config_back')],
|
||
]
|
||
)
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def _get_device_declension(count: int) -> str:
|
||
if count % 10 == 1 and count % 100 != 11:
|
||
return 'устройство'
|
||
if count % 10 in [2, 3, 4] and count % 100 not in [12, 13, 14]:
|
||
return 'устройства'
|
||
return 'устройств'
|
||
|
||
|
||
def get_subscription_confirm_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(text=texts.CONFIRM, callback_data='subscription_confirm'),
|
||
InlineKeyboardButton(text=texts.CANCEL, callback_data='subscription_cancel'),
|
||
]
|
||
]
|
||
)
|
||
|
||
|
||
def get_balance_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton(text=texts.BALANCE_HISTORY, callback_data='balance_history'),
|
||
InlineKeyboardButton(text=texts.BALANCE_TOP_UP, callback_data='balance_topup'),
|
||
],
|
||
[InlineKeyboardButton(text=texts.BACK, callback_data='back_to_menu')],
|
||
]
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_payment_methods_keyboard(amount_kopeks: int, language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
keyboard = []
|
||
has_direct_payment_methods = False
|
||
|
||
amount_kopeks = max(0, int(amount_kopeks or 0))
|
||
|
||
def _build_callback(method: str) -> str:
|
||
if amount_kopeks > 0:
|
||
return f'topup_amount|{method}|{amount_kopeks}'
|
||
return f'topup_{method}'
|
||
|
||
if settings.TELEGRAM_STARS_ENABLED:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAYMENT_TELEGRAM_STARS', '⭐ Telegram Stars'), callback_data=_build_callback('stars')
|
||
)
|
||
]
|
||
)
|
||
has_direct_payment_methods = True
|
||
|
||
if settings.is_yookassa_enabled():
|
||
if settings.YOOKASSA_SBP_ENABLED:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAYMENT_SBP_YOOKASSA', '🏦 Оплатить по СБП (YooKassa)'),
|
||
callback_data=_build_callback('yookassa_sbp'),
|
||
)
|
||
]
|
||
)
|
||
has_direct_payment_methods = True
|
||
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAYMENT_CARD_YOOKASSA', '💳 Банковская карта (YooKassa)'),
|
||
callback_data=_build_callback('yookassa'),
|
||
)
|
||
]
|
||
)
|
||
has_direct_payment_methods = True
|
||
|
||
if settings.TRIBUTE_ENABLED:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAYMENT_CARD_TRIBUTE', '💳 Банковская карта (Tribute)'),
|
||
callback_data=_build_callback('tribute'),
|
||
)
|
||
]
|
||
)
|
||
has_direct_payment_methods = True
|
||
|
||
if settings.is_mulenpay_enabled():
|
||
mulenpay_name = settings.get_mulenpay_display_name()
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t(
|
||
'PAYMENT_CARD_MULENPAY',
|
||
'💳 Банковская карта ({mulenpay_name})',
|
||
).format(mulenpay_name=mulenpay_name),
|
||
callback_data=_build_callback('mulenpay'),
|
||
)
|
||
]
|
||
)
|
||
has_direct_payment_methods = True
|
||
|
||
if settings.is_wata_enabled():
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAYMENT_CARD_WATA', '💳 Банковская карта (WATA)'),
|
||
callback_data=_build_callback('wata'),
|
||
)
|
||
]
|
||
)
|
||
has_direct_payment_methods = True
|
||
|
||
if settings.is_pal24_enabled():
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAYMENT_CARD_PAL24', '🏦 СБП (PayPalych)'), callback_data=_build_callback('pal24')
|
||
)
|
||
]
|
||
)
|
||
has_direct_payment_methods = True
|
||
|
||
if settings.is_platega_enabled() and settings.get_platega_active_methods():
|
||
platega_name = settings.get_platega_display_name()
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAYMENT_PLATEGA', f'💳 {platega_name}'),
|
||
callback_data=_build_callback('platega'),
|
||
)
|
||
]
|
||
)
|
||
has_direct_payment_methods = True
|
||
|
||
if settings.is_cryptobot_enabled():
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAYMENT_CRYPTOBOT', '🪙 Криптовалюта (CryptoBot)'),
|
||
callback_data=_build_callback('cryptobot'),
|
||
)
|
||
]
|
||
)
|
||
has_direct_payment_methods = True
|
||
|
||
if settings.is_heleket_enabled():
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAYMENT_HELEKET', '🪙 Криптовалюта (Heleket)'),
|
||
callback_data=_build_callback('heleket'),
|
||
)
|
||
]
|
||
)
|
||
has_direct_payment_methods = True
|
||
|
||
if settings.is_cloudpayments_enabled():
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAYMENT_CLOUDPAYMENTS', '💳 Банковская карта (CloudPayments)'),
|
||
callback_data=_build_callback('cloudpayments'),
|
||
)
|
||
]
|
||
)
|
||
has_direct_payment_methods = True
|
||
|
||
if settings.is_freekassa_enabled():
|
||
freekassa_name = settings.get_freekassa_display_name()
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAYMENT_FREEKASSA', f'💳 {freekassa_name}'),
|
||
callback_data=_build_callback('freekassa'),
|
||
)
|
||
]
|
||
)
|
||
has_direct_payment_methods = True
|
||
|
||
if settings.is_kassa_ai_enabled():
|
||
kassa_ai_name = settings.get_kassa_ai_display_name()
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAYMENT_KASSA_AI', f'💳 {kassa_ai_name}'), callback_data=_build_callback('kassa_ai')
|
||
)
|
||
]
|
||
)
|
||
has_direct_payment_methods = True
|
||
|
||
if settings.is_support_topup_enabled():
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAYMENT_VIA_SUPPORT', '🛠️ Через поддержку'), callback_data='topup_support'
|
||
)
|
||
]
|
||
)
|
||
|
||
if not keyboard:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAYMENTS_TEMPORARILY_UNAVAILABLE', '⚠️ Способы оплаты временно недоступны'),
|
||
callback_data='payment_methods_unavailable',
|
||
)
|
||
]
|
||
)
|
||
elif not has_direct_payment_methods and settings.is_support_topup_enabled():
|
||
keyboard.insert(
|
||
0,
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAYMENTS_TEMPORARILY_UNAVAILABLE', '⚠️ Способы оплаты временно недоступны'),
|
||
callback_data='payment_methods_unavailable',
|
||
)
|
||
],
|
||
)
|
||
|
||
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_balance')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_yookassa_payment_keyboard(
|
||
payment_id: str, amount_kopeks: int, confirmation_url: str, language: str = DEFAULT_LANGUAGE
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[InlineKeyboardButton(text=texts.t('PAY_NOW_BUTTON', '💳 Оплатить'), url=confirmation_url)],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CHECK_STATUS_BUTTON', '📊 Проверить статус'),
|
||
callback_data=f'check_yookassa_status_{payment_id}',
|
||
)
|
||
],
|
||
[InlineKeyboardButton(text=texts.t('MY_BALANCE_BUTTON', '💰 Мой баланс'), callback_data='menu_balance')],
|
||
]
|
||
)
|
||
|
||
|
||
def get_autopay_notification_keyboard(subscription_id: int, language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
build_miniapp_or_callback_button(
|
||
text=texts.t('TOPUP_BALANCE_BUTTON', '💳 Пополнить баланс'), callback_data='balance_topup'
|
||
)
|
||
],
|
||
[
|
||
build_miniapp_or_callback_button(
|
||
text=texts.t('MY_SUBSCRIPTION_BUTTON', '📱 Моя подписка'), callback_data='menu_subscription'
|
||
)
|
||
],
|
||
]
|
||
)
|
||
|
||
|
||
def get_subscription_expiring_keyboard(subscription_id: int, language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
build_miniapp_or_callback_button(
|
||
text=texts.MENU_EXTEND_SUBSCRIPTION, callback_data='subscription_extend'
|
||
)
|
||
],
|
||
[
|
||
build_miniapp_or_callback_button(
|
||
text=texts.t('TOPUP_BALANCE_BUTTON', '💳 Пополнить баланс'), callback_data='balance_topup'
|
||
)
|
||
],
|
||
[
|
||
build_miniapp_or_callback_button(
|
||
text=texts.t('MY_SUBSCRIPTION_BUTTON', '📱 Моя подписка'), callback_data='menu_subscription'
|
||
)
|
||
],
|
||
]
|
||
)
|
||
|
||
|
||
def get_referral_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CREATE_INVITE_BUTTON', '📝 Создать приглашение'), callback_data='referral_create_invite'
|
||
)
|
||
],
|
||
[InlineKeyboardButton(text=texts.t('SHOW_QR_BUTTON', '📱 Показать QR код'), callback_data='referral_show_qr')],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('REFERRAL_LIST_BUTTON', '👥 Список рефералов'), callback_data='referral_list'
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('REFERRAL_ANALYTICS_BUTTON', '📊 Аналитика'), callback_data='referral_analytics'
|
||
)
|
||
],
|
||
]
|
||
|
||
# Добавляем кнопку вывода, если включена
|
||
if settings.is_referral_withdrawal_enabled():
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('REFERRAL_WITHDRAWAL_BUTTON', '💸 Запросить вывод'),
|
||
callback_data='referral_withdrawal',
|
||
)
|
||
]
|
||
)
|
||
|
||
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='back_to_menu')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_support_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
try:
|
||
from app.services.support_settings_service import SupportSettingsService
|
||
|
||
tickets_enabled = SupportSettingsService.is_tickets_enabled()
|
||
contact_enabled = SupportSettingsService.is_contact_enabled()
|
||
except Exception:
|
||
tickets_enabled = True
|
||
contact_enabled = True
|
||
rows: list[list[InlineKeyboardButton]] = []
|
||
# Tickets
|
||
if tickets_enabled:
|
||
rows.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CREATE_TICKET_BUTTON', '🎫 Создать тикет'), callback_data='create_ticket'
|
||
)
|
||
]
|
||
)
|
||
rows.append(
|
||
[InlineKeyboardButton(text=texts.t('MY_TICKETS_BUTTON', '📋 Мои тикеты'), callback_data='my_tickets')]
|
||
)
|
||
# Direct contact
|
||
if contact_enabled and settings.get_support_contact_url():
|
||
rows.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CONTACT_SUPPORT_BUTTON', '💬 Связаться с поддержкой'),
|
||
url=settings.get_support_contact_url() or 'https://t.me/',
|
||
)
|
||
]
|
||
)
|
||
rows.append([InlineKeyboardButton(text=texts.BACK, callback_data='back_to_menu')])
|
||
return InlineKeyboardMarkup(inline_keyboard=rows)
|
||
|
||
|
||
def get_pagination_keyboard(
|
||
current_page: int, total_pages: int, callback_prefix: str, language: str = DEFAULT_LANGUAGE
|
||
) -> list[list[InlineKeyboardButton]]:
|
||
texts = get_texts(language)
|
||
keyboard = []
|
||
|
||
if total_pages > 1:
|
||
row = []
|
||
|
||
if current_page > 1:
|
||
row.append(
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAGINATION_PREV', '⬅️'), callback_data=f'{callback_prefix}_page_{current_page - 1}'
|
||
)
|
||
)
|
||
|
||
row.append(InlineKeyboardButton(text=f'{current_page}/{total_pages}', callback_data='current_page'))
|
||
|
||
if current_page < total_pages:
|
||
row.append(
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAGINATION_NEXT', '➡️'), callback_data=f'{callback_prefix}_page_{current_page + 1}'
|
||
)
|
||
)
|
||
|
||
keyboard.append(row)
|
||
|
||
return keyboard
|
||
|
||
|
||
def get_confirmation_keyboard(
|
||
confirm_data: str, cancel_data: str = 'cancel', language: str = DEFAULT_LANGUAGE
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(text=texts.YES, callback_data=confirm_data),
|
||
InlineKeyboardButton(text=texts.NO, callback_data=cancel_data),
|
||
]
|
||
]
|
||
)
|
||
|
||
|
||
def get_autopay_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(text=texts.t('ENABLE_BUTTON', '✅ Включить'), callback_data='autopay_enable'),
|
||
InlineKeyboardButton(text=texts.t('DISABLE_BUTTON', '❌ Выключить'), callback_data='autopay_disable'),
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('AUTOPAY_SET_DAYS_BUTTON', '⚙️ Настроить дни'), callback_data='autopay_set_days'
|
||
)
|
||
],
|
||
[InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')],
|
||
]
|
||
)
|
||
|
||
|
||
def get_autopay_days_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
keyboard = []
|
||
|
||
for days in [1, 3, 7, 14]:
|
||
keyboard.append(
|
||
[InlineKeyboardButton(text=f'{days} {_get_days_word(days)}', callback_data=f'autopay_days_{days}')]
|
||
)
|
||
|
||
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='subscription_autopay')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def _get_days_word(days: int) -> str:
|
||
if days % 10 == 1 and days % 100 != 11:
|
||
return 'день'
|
||
if 2 <= days % 10 <= 4 and not (12 <= days % 100 <= 14):
|
||
return 'дня'
|
||
return 'дней'
|
||
|
||
|
||
# Deprecated: get_extend_subscription_keyboard() was removed.
|
||
# Use get_extend_subscription_keyboard_with_prices() instead for personalized pricing.
|
||
|
||
|
||
def get_add_traffic_keyboard(
|
||
language: str = DEFAULT_LANGUAGE,
|
||
subscription_end_date: datetime = None,
|
||
discount_percent: int = 0,
|
||
) -> InlineKeyboardMarkup:
|
||
from app.config import settings
|
||
from app.utils.pricing_utils import get_remaining_months
|
||
|
||
texts = get_texts(language)
|
||
language_code = (language or DEFAULT_LANGUAGE).split('-')[0].lower()
|
||
use_russian_fallback = language_code in {'ru', 'fa'}
|
||
|
||
months_multiplier = 1
|
||
period_text = ''
|
||
if subscription_end_date:
|
||
months_multiplier = get_remaining_months(subscription_end_date)
|
||
if months_multiplier > 1:
|
||
period_text = f' (за {months_multiplier} мес)'
|
||
|
||
packages = settings.get_traffic_topup_packages()
|
||
enabled_packages = [pkg for pkg in packages if pkg['enabled']]
|
||
|
||
if not enabled_packages:
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('NO_TRAFFIC_PACKAGES', '❌ Нет доступных пакетов'),
|
||
callback_data='no_traffic_packages',
|
||
)
|
||
],
|
||
[InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')],
|
||
]
|
||
)
|
||
|
||
buttons = []
|
||
|
||
for package in enabled_packages:
|
||
gb = package['gb']
|
||
price_per_month = package['price']
|
||
discounted_per_month, discount_per_month = apply_percentage_discount(
|
||
price_per_month,
|
||
discount_percent,
|
||
)
|
||
total_price = discounted_per_month * months_multiplier
|
||
total_discount = discount_per_month * months_multiplier
|
||
|
||
if gb == 0:
|
||
if use_russian_fallback:
|
||
text = f'♾️ Безлимитный трафик - {total_price // 100} ₽{period_text}'
|
||
else:
|
||
text = f'♾️ Unlimited traffic - {total_price // 100} ₽{period_text}'
|
||
elif use_russian_fallback:
|
||
text = f'📊 +{gb} ГБ трафика - {total_price // 100} ₽{period_text}'
|
||
else:
|
||
text = f'📊 +{gb} GB traffic - {total_price // 100} ₽{period_text}'
|
||
|
||
if discount_percent > 0 and total_discount > 0:
|
||
if use_russian_fallback:
|
||
text += f' (скидка {discount_percent}%: -{total_discount // 100}₽)'
|
||
else:
|
||
text += f' (discount {discount_percent}%: -{total_discount // 100}₽)'
|
||
|
||
buttons.append([InlineKeyboardButton(text=text, callback_data=f'add_traffic_{gb}')])
|
||
|
||
buttons.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
|
||
def get_add_traffic_keyboard_from_tariff(
|
||
language: str,
|
||
packages: dict, # {gb: price_kopeks}
|
||
subscription_end_date: datetime = None,
|
||
discount_percent: int = 0,
|
||
) -> InlineKeyboardMarkup:
|
||
"""
|
||
Клавиатура для докупки трафика из настроек тарифа.
|
||
|
||
Args:
|
||
language: Язык интерфейса
|
||
packages: Словарь {ГБ: цена_в_копейках} из тарифа
|
||
subscription_end_date: Дата окончания подписки для расчета цены
|
||
discount_percent: Процент скидки
|
||
"""
|
||
texts = get_texts(language)
|
||
language_code = (language or DEFAULT_LANGUAGE).split('-')[0].lower()
|
||
use_russian_fallback = language_code in {'ru', 'fa'}
|
||
|
||
if not packages:
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('NO_TRAFFIC_PACKAGES', '❌ Нет доступных пакетов'),
|
||
callback_data='no_traffic_packages',
|
||
)
|
||
],
|
||
[InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')],
|
||
]
|
||
)
|
||
|
||
buttons = []
|
||
|
||
# Сортируем пакеты по размеру
|
||
sorted_packages = sorted(packages.items(), key=lambda x: x[0])
|
||
|
||
# Пакеты трафика на тарифах покупаются на 1 месяц (30 дней),
|
||
# цена в тарифе уже месячная — не умножаем на оставшиеся месяцы подписки
|
||
for gb, price_per_month in sorted_packages:
|
||
discounted_price, discount_value = apply_percentage_discount(
|
||
price_per_month,
|
||
discount_percent,
|
||
)
|
||
|
||
period_text = ' /мес' if use_russian_fallback else ' /mo'
|
||
|
||
if use_russian_fallback:
|
||
text = f'📊 +{gb} ГБ трафика - {discounted_price // 100} ₽{period_text}'
|
||
else:
|
||
text = f'📊 +{gb} GB traffic - {discounted_price // 100} ₽{period_text}'
|
||
|
||
if discount_percent > 0 and discount_value > 0:
|
||
if use_russian_fallback:
|
||
text += f' (скидка {discount_percent}%: -{discount_value // 100}₽)'
|
||
else:
|
||
text += f' (discount {discount_percent}%: -{discount_value // 100}₽)'
|
||
|
||
buttons.append([InlineKeyboardButton(text=text, callback_data=f'add_traffic_{gb}')])
|
||
|
||
buttons.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
|
||
def get_change_devices_keyboard(
|
||
current_devices: int,
|
||
language: str = DEFAULT_LANGUAGE,
|
||
subscription_end_date: datetime = None,
|
||
discount_percent: int = 0,
|
||
tariff=None, # Тариф для цены за устройство
|
||
) -> InlineKeyboardMarkup:
|
||
from app.config import settings
|
||
from app.utils.pricing_utils import get_remaining_months
|
||
|
||
texts = get_texts(language)
|
||
|
||
# Проверяем является ли тариф суточным
|
||
is_daily_tariff = tariff and getattr(tariff, 'is_daily', False)
|
||
|
||
# Для суточных тарифов считаем по дням, для обычных - по месяцам
|
||
if is_daily_tariff and subscription_end_date:
|
||
# Суточный тариф: цена за оставшиеся дни (обычно 1 день)
|
||
from datetime import datetime
|
||
|
||
now = datetime.utcnow()
|
||
days_left = max(1, (subscription_end_date - now).days)
|
||
# Множитель = days_left / 30 (как в кабинете)
|
||
price_multiplier = days_left / 30
|
||
period_text = f' (за {days_left} дн.)' if days_left > 1 else ' (за 1 день)'
|
||
else:
|
||
# Обычный тариф: цена за оставшиеся месяцы
|
||
months_multiplier = 1
|
||
period_text = ''
|
||
if subscription_end_date:
|
||
months_multiplier = get_remaining_months(subscription_end_date)
|
||
if months_multiplier > 1:
|
||
period_text = f' (за {months_multiplier} мес)'
|
||
price_multiplier = months_multiplier
|
||
|
||
# Используем цену из тарифа если есть, иначе глобальную настройку
|
||
tariff_device_price = getattr(tariff, 'device_price_kopeks', None) if tariff else None
|
||
if tariff and tariff_device_price:
|
||
device_price_per_month = tariff_device_price
|
||
# Для тарифов все устройства платные (нет бесплатного лимита)
|
||
default_device_limit = 0
|
||
else:
|
||
device_price_per_month = settings.PRICE_PER_DEVICE
|
||
default_device_limit = settings.DEFAULT_DEVICE_LIMIT
|
||
|
||
buttons = []
|
||
|
||
# Используем max_device_limit из тарифа если есть, иначе глобальную настройку
|
||
tariff_max_devices = getattr(tariff, 'max_device_limit', None) if tariff else None
|
||
if tariff_max_devices and tariff_max_devices > 0:
|
||
max_devices = tariff_max_devices
|
||
else:
|
||
max_devices = settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else 20
|
||
|
||
# Минимальное количество устройств: device_limit тарифа или 1
|
||
tariff_min_devices = (getattr(tariff, 'device_limit', 1) or 1) if tariff else 1
|
||
min_devices = max(1, tariff_min_devices)
|
||
|
||
start_range = max(min_devices, min(current_devices - 3, max_devices - 6))
|
||
end_range = min(max_devices + 1, max(current_devices + 4, 7))
|
||
|
||
for devices_count in range(start_range, end_range):
|
||
if devices_count == current_devices:
|
||
emoji = '✅'
|
||
action_text = ' (текущее)'
|
||
price_text = ''
|
||
elif devices_count > current_devices:
|
||
emoji = '➕'
|
||
devices_count - current_devices
|
||
|
||
current_chargeable = max(0, current_devices - default_device_limit)
|
||
new_chargeable = max(0, devices_count - default_device_limit)
|
||
chargeable_devices = new_chargeable - current_chargeable
|
||
|
||
if chargeable_devices > 0:
|
||
price_per_month = chargeable_devices * device_price_per_month
|
||
discounted_per_month, discount_per_month = apply_percentage_discount(
|
||
price_per_month,
|
||
discount_percent,
|
||
)
|
||
total_price = int(discounted_per_month * price_multiplier)
|
||
total_price = max(100, total_price) # Минимум 1 рубль
|
||
price_text = f' (+{total_price // 100}₽{period_text})'
|
||
total_discount = int(discount_per_month * price_multiplier)
|
||
if discount_percent > 0 and total_discount > 0:
|
||
price_text += f' (скидка {discount_percent}%: -{total_discount // 100}₽)'
|
||
action_text = ''
|
||
else:
|
||
price_text = ' (бесплатно)'
|
||
action_text = ''
|
||
else:
|
||
emoji = '➖'
|
||
action_text = ''
|
||
price_text = ' (без возврата)'
|
||
|
||
button_text = f'{emoji} {devices_count} устр.{action_text}{price_text}'
|
||
|
||
buttons.append([InlineKeyboardButton(text=button_text, callback_data=f'change_devices_{devices_count}')])
|
||
|
||
if current_devices < start_range or current_devices >= end_range:
|
||
current_button = f'✅ {current_devices} устр. (текущее)'
|
||
buttons.insert(
|
||
0, [InlineKeyboardButton(text=current_button, callback_data=f'change_devices_{current_devices}')]
|
||
)
|
||
|
||
buttons.append([InlineKeyboardButton(text=texts.BACK, callback_data='subscription_settings')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
|
||
def get_confirm_change_devices_keyboard(
|
||
new_devices_count: int, price: int, language: str = DEFAULT_LANGUAGE
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CONFIRM_CHANGE_BUTTON', '✅ Подтвердить изменение'),
|
||
callback_data=f'confirm_change_devices_{new_devices_count}_{price}',
|
||
)
|
||
],
|
||
[InlineKeyboardButton(text=texts.CANCEL, callback_data='subscription_settings')],
|
||
]
|
||
)
|
||
|
||
|
||
def get_reset_traffic_confirm_keyboard(
|
||
price_kopeks: int,
|
||
language: str = DEFAULT_LANGUAGE,
|
||
has_enough_balance: bool = True,
|
||
missing_kopeks: int = 0,
|
||
) -> InlineKeyboardMarkup:
|
||
from app.config import settings
|
||
|
||
if settings.is_traffic_topup_blocked():
|
||
return get_back_keyboard(language)
|
||
|
||
texts = get_texts(language)
|
||
buttons = []
|
||
|
||
if has_enough_balance:
|
||
# Достаточно средств - показываем кнопку сброса
|
||
buttons.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=f'✅ Сбросить за {settings.format_price(price_kopeks)}', callback_data='confirm_reset_traffic'
|
||
)
|
||
]
|
||
)
|
||
else:
|
||
# Не хватает средств - показываем кнопку пополнения
|
||
buttons.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('TOPUP_BALANCE_BUTTON', '💳 Пополнить баланс'),
|
||
callback_data='balance_topup',
|
||
)
|
||
]
|
||
)
|
||
|
||
buttons.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.BACK,
|
||
callback_data='subscription_settings',
|
||
)
|
||
]
|
||
)
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
|
||
def get_manage_countries_keyboard(
|
||
countries: list[dict],
|
||
selected: list[str],
|
||
current_subscription_countries: list[str],
|
||
language: str = DEFAULT_LANGUAGE,
|
||
subscription_end_date: datetime = None,
|
||
discount_percent: int = 0,
|
||
) -> InlineKeyboardMarkup:
|
||
from app.utils.pricing_utils import get_remaining_months
|
||
|
||
texts = get_texts(language)
|
||
|
||
months_multiplier = 1
|
||
if subscription_end_date:
|
||
months_multiplier = get_remaining_months(subscription_end_date)
|
||
logger.info(
|
||
f'🔍 Расчет для управления странами: осталось {months_multiplier} месяцев до {subscription_end_date}'
|
||
)
|
||
|
||
buttons = []
|
||
total_cost = 0
|
||
|
||
for country in countries:
|
||
if not country.get('is_available', True):
|
||
continue
|
||
|
||
uuid = country['uuid']
|
||
name = country['name']
|
||
price_per_month = country['price_kopeks']
|
||
|
||
discounted_per_month, discount_per_month = apply_percentage_discount(
|
||
price_per_month,
|
||
discount_percent,
|
||
)
|
||
|
||
if uuid in current_subscription_countries:
|
||
if uuid in selected:
|
||
icon = '✅'
|
||
else:
|
||
icon = '➖'
|
||
elif uuid in selected:
|
||
icon = '➕'
|
||
total_cost += discounted_per_month * months_multiplier
|
||
else:
|
||
icon = '⚪'
|
||
|
||
if uuid not in current_subscription_countries and uuid in selected:
|
||
total_price = discounted_per_month * months_multiplier
|
||
if months_multiplier > 1:
|
||
price_text = f' ({discounted_per_month // 100}₽/мес × {months_multiplier} = {total_price // 100}₽)'
|
||
logger.info(
|
||
'🔍 Сервер %s: %.2f₽/мес × %s мес = %.2f₽ (скидка %.2f₽)',
|
||
name,
|
||
discounted_per_month / 100,
|
||
months_multiplier,
|
||
total_price / 100,
|
||
(discount_per_month * months_multiplier) / 100,
|
||
)
|
||
else:
|
||
price_text = f' ({total_price // 100}₽)'
|
||
if discount_percent > 0 and discount_per_month * months_multiplier > 0:
|
||
price_text += f' (скидка {discount_percent}%: -{(discount_per_month * months_multiplier) // 100}₽)'
|
||
display_name = f'{icon} {name}{price_text}'
|
||
else:
|
||
display_name = f'{icon} {name}'
|
||
|
||
buttons.append([InlineKeyboardButton(text=display_name, callback_data=f'country_manage_{uuid}')])
|
||
|
||
if total_cost > 0:
|
||
apply_text = f'✅ Применить изменения ({total_cost // 100} ₽)'
|
||
logger.info(f'🔍 Общая стоимость новых серверов: {total_cost / 100}₽')
|
||
else:
|
||
apply_text = '✅ Применить изменения'
|
||
|
||
buttons.append([InlineKeyboardButton(text=apply_text, callback_data='countries_apply')])
|
||
|
||
buttons.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
|
||
def get_device_selection_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
from app.config import settings
|
||
|
||
texts = get_texts(language)
|
||
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('DEVICE_GUIDE_IOS', '📱 iOS (iPhone/iPad)'), callback_data='device_guide_ios'
|
||
),
|
||
InlineKeyboardButton(
|
||
text=texts.t('DEVICE_GUIDE_ANDROID', '🤖 Android'), callback_data='device_guide_android'
|
||
),
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('DEVICE_GUIDE_WINDOWS', '💻 Windows'), callback_data='device_guide_windows'
|
||
),
|
||
InlineKeyboardButton(text=texts.t('DEVICE_GUIDE_MAC', '🎯 macOS'), callback_data='device_guide_mac'),
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('DEVICE_GUIDE_ANDROID_TV', '📺 Android TV'), callback_data='device_guide_tv'
|
||
),
|
||
InlineKeyboardButton(
|
||
text=texts.t('DEVICE_GUIDE_APPLE_TV', '📺 Apple TV'), callback_data='device_guide_appletv'
|
||
),
|
||
],
|
||
]
|
||
|
||
if settings.CONNECT_BUTTON_MODE == 'guide':
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('SHOW_SUBSCRIPTION_LINK', '📋 Показать ссылку подписки'),
|
||
callback_data='open_subscription_link',
|
||
)
|
||
]
|
||
)
|
||
|
||
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_connection_guide_keyboard(
|
||
subscription_url: str,
|
||
app: dict,
|
||
device_type: str,
|
||
language: str = DEFAULT_LANGUAGE,
|
||
has_other_apps: bool = False,
|
||
) -> InlineKeyboardMarkup:
|
||
from app.handlers.subscription import create_deep_link
|
||
|
||
texts = get_texts(language)
|
||
|
||
keyboard = []
|
||
|
||
if 'installationStep' in app and 'buttons' in app['installationStep']:
|
||
app_buttons = []
|
||
for button in app['installationStep']['buttons']:
|
||
button_text = _get_localized_value(button.get('buttonText'), language)
|
||
button_link = button.get('buttonLink')
|
||
|
||
if not button_text or not button_link:
|
||
continue
|
||
|
||
app_buttons.append(InlineKeyboardButton(text=f'📥 {button_text}', url=button_link))
|
||
if len(app_buttons) == 2:
|
||
keyboard.append(app_buttons)
|
||
app_buttons = []
|
||
|
||
if app_buttons:
|
||
keyboard.append(app_buttons)
|
||
|
||
additional_before_buttons = _build_additional_buttons(
|
||
app.get('additionalBeforeAddSubscriptionStep'),
|
||
language,
|
||
)
|
||
|
||
for button in additional_before_buttons:
|
||
keyboard.append([button])
|
||
|
||
connect_link = create_deep_link(app, subscription_url)
|
||
|
||
if connect_link:
|
||
connect_button = InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
||
url=connect_link,
|
||
)
|
||
elif settings.is_happ_cryptolink_mode():
|
||
connect_button = InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
||
callback_data='open_subscription_link',
|
||
)
|
||
else:
|
||
connect_button = InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
||
url=subscription_url,
|
||
)
|
||
|
||
keyboard.append([connect_button])
|
||
|
||
additional_after_buttons = _build_additional_buttons(
|
||
app.get('additionalAfterAddSubscriptionStep'),
|
||
language,
|
||
)
|
||
|
||
for button in additional_after_buttons:
|
||
keyboard.append([button])
|
||
|
||
if has_other_apps:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('OTHER_APPS_BUTTON', '📋 Другие приложения'),
|
||
callback_data=f'app_list_{device_type}',
|
||
)
|
||
]
|
||
)
|
||
|
||
keyboard.extend(
|
||
[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CHOOSE_ANOTHER_DEVICE', '📱 Выбрать другое устройство'),
|
||
callback_data='subscription_connect',
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('BACK_TO_SUBSCRIPTION', '⬅️ К подписке'), callback_data='menu_subscription'
|
||
)
|
||
],
|
||
]
|
||
)
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_app_selection_keyboard(device_type: str, apps: list, language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
keyboard = []
|
||
|
||
for app in apps:
|
||
app_name = app['name']
|
||
if app.get('isFeatured', False):
|
||
app_name = f'⭐ {app_name}'
|
||
|
||
keyboard.append([InlineKeyboardButton(text=app_name, callback_data=f'app_{device_type}_{app["id"]}')])
|
||
|
||
keyboard.extend(
|
||
[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CHOOSE_ANOTHER_DEVICE', '📱 Выбрать другое устройство'),
|
||
callback_data='subscription_connect',
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('BACK_TO_SUBSCRIPTION', '⬅️ К подписке'), callback_data='menu_subscription'
|
||
)
|
||
],
|
||
]
|
||
)
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_specific_app_keyboard(
|
||
subscription_url: str, app: dict, device_type: str, language: str = DEFAULT_LANGUAGE
|
||
) -> InlineKeyboardMarkup:
|
||
from app.handlers.subscription import create_deep_link
|
||
|
||
texts = get_texts(language)
|
||
|
||
keyboard = []
|
||
|
||
if 'installationStep' in app and 'buttons' in app['installationStep']:
|
||
app_buttons = []
|
||
for button in app['installationStep']['buttons']:
|
||
button_text = _get_localized_value(button.get('buttonText'), language)
|
||
button_link = button.get('buttonLink')
|
||
|
||
if not button_text or not button_link:
|
||
continue
|
||
|
||
app_buttons.append(InlineKeyboardButton(text=f'📥 {button_text}', url=button_link))
|
||
if len(app_buttons) == 2:
|
||
keyboard.append(app_buttons)
|
||
app_buttons = []
|
||
|
||
if app_buttons:
|
||
keyboard.append(app_buttons)
|
||
|
||
additional_before_buttons = _build_additional_buttons(
|
||
app.get('additionalBeforeAddSubscriptionStep'),
|
||
language,
|
||
)
|
||
|
||
for button in additional_before_buttons:
|
||
keyboard.append([button])
|
||
|
||
connect_link = create_deep_link(app, subscription_url)
|
||
|
||
if connect_link:
|
||
connect_button = InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
||
url=connect_link,
|
||
)
|
||
elif settings.is_happ_cryptolink_mode():
|
||
connect_button = InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
||
callback_data='open_subscription_link',
|
||
)
|
||
else:
|
||
connect_button = InlineKeyboardButton(
|
||
text=texts.t('CONNECT_BUTTON', '🔗 Подключиться'),
|
||
url=subscription_url,
|
||
)
|
||
|
||
keyboard.append([connect_button])
|
||
|
||
additional_after_buttons = _build_additional_buttons(
|
||
app.get('additionalAfterAddSubscriptionStep'),
|
||
language,
|
||
)
|
||
|
||
for button in additional_after_buttons:
|
||
keyboard.append([button])
|
||
|
||
keyboard.extend(
|
||
[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('OTHER_APPS_BUTTON', '📋 Другие приложения'), callback_data=f'app_list_{device_type}'
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CHOOSE_ANOTHER_DEVICE', '📱 Выбрать другое устройство'),
|
||
callback_data='subscription_connect',
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('BACK_TO_SUBSCRIPTION', '⬅️ К подписке'), callback_data='menu_subscription'
|
||
)
|
||
],
|
||
]
|
||
)
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_extend_subscription_keyboard_with_prices(language: str, prices: dict) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
keyboard = []
|
||
|
||
available_periods = settings.get_available_renewal_periods()
|
||
|
||
for days in available_periods:
|
||
if days not in prices:
|
||
continue
|
||
|
||
price_info = prices[days]
|
||
|
||
if isinstance(price_info, dict):
|
||
final_price = price_info.get('final')
|
||
original_price = price_info.get('original', 0)
|
||
if final_price is None:
|
||
final_price = price_info.get('original', 0)
|
||
else:
|
||
final_price = price_info
|
||
original_price = price_info
|
||
|
||
period_display = format_period_description(days, language)
|
||
|
||
# Create PriceInfo from already calculated prices
|
||
# Note: original_price and final_price are calculated in the handler
|
||
discount_percent = 0
|
||
if original_price > final_price and original_price > 0:
|
||
discount_percent = ((original_price - final_price) * 100) // original_price
|
||
|
||
price_info_obj = PriceInfo(
|
||
base_price=original_price, final_price=final_price, discount_percent=discount_percent
|
||
)
|
||
|
||
# Format button using unified system
|
||
button_text = format_price_button(
|
||
period_label=period_display,
|
||
price_info=price_info_obj,
|
||
format_price_func=texts.format_price,
|
||
emphasize=False,
|
||
add_exclamation=False,
|
||
)
|
||
|
||
keyboard.append([InlineKeyboardButton(text=button_text, callback_data=f'extend_period_{days}')])
|
||
|
||
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_cryptobot_payment_keyboard(
|
||
payment_id: str,
|
||
local_payment_id: int,
|
||
amount_usd: float,
|
||
asset: str,
|
||
bot_invoice_url: str,
|
||
language: str = DEFAULT_LANGUAGE,
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[InlineKeyboardButton(text=texts.t('PAY_WITH_COINS_BUTTON', '🪙 Оплатить'), url=bot_invoice_url)],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CHECK_STATUS_BUTTON', '📊 Проверить статус'),
|
||
callback_data=f'check_cryptobot_{local_payment_id}',
|
||
)
|
||
],
|
||
[InlineKeyboardButton(text=texts.t('MY_BALANCE_BUTTON', '💰 Мой баланс'), callback_data='menu_balance')],
|
||
]
|
||
)
|
||
|
||
|
||
def get_devices_management_keyboard(
|
||
devices: list[dict], pagination, language: str = DEFAULT_LANGUAGE
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
|
||
keyboard = []
|
||
|
||
for i, device in enumerate(devices):
|
||
platform = device.get('platform', 'Unknown')
|
||
device_model = device.get('deviceModel', 'Unknown')
|
||
device_info = f'{platform} - {device_model}'
|
||
|
||
if len(device_info) > 25:
|
||
device_info = device_info[:22] + '...'
|
||
|
||
keyboard.append(
|
||
[InlineKeyboardButton(text=f'🔄 {device_info}', callback_data=f'reset_device_{i}_{pagination.page}')]
|
||
)
|
||
|
||
if pagination.total_pages > 1:
|
||
nav_row = []
|
||
|
||
if pagination.has_prev:
|
||
nav_row.append(
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAGINATION_PREV', '⬅️'), callback_data=f'devices_page_{pagination.prev_page}'
|
||
)
|
||
)
|
||
|
||
nav_row.append(
|
||
InlineKeyboardButton(text=f'{pagination.page}/{pagination.total_pages}', callback_data='current_page')
|
||
)
|
||
|
||
if pagination.has_next:
|
||
nav_row.append(
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAGINATION_NEXT', '➡️'), callback_data=f'devices_page_{pagination.next_page}'
|
||
)
|
||
)
|
||
|
||
keyboard.append(nav_row)
|
||
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('RESET_ALL_DEVICES_BUTTON', '🔄 Сбросить все устройства'),
|
||
callback_data='reset_all_devices',
|
||
)
|
||
]
|
||
)
|
||
|
||
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='subscription_settings')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_updated_subscription_settings_keyboard(
|
||
language: str = DEFAULT_LANGUAGE,
|
||
show_countries_management: bool = True,
|
||
tariff=None, # Тариф подписки (если есть - ограничиваем настройки)
|
||
subscription=None, # Подписка (для проверки суточной паузы)
|
||
) -> InlineKeyboardMarkup:
|
||
from app.config import settings
|
||
|
||
texts = get_texts(language)
|
||
keyboard = []
|
||
|
||
# Если подписка на тарифе - отключаем страны, модем, трафик
|
||
has_tariff = tariff is not None
|
||
|
||
# Для суточных тарифов кнопка паузы теперь в главном меню подписки
|
||
|
||
if show_countries_management and not has_tariff:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('ADD_COUNTRIES_BUTTON', '🌐 Добавить страны'),
|
||
callback_data='subscription_add_countries',
|
||
)
|
||
]
|
||
)
|
||
|
||
if settings.is_traffic_selectable() and not has_tariff:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('RESET_TRAFFIC_BUTTON', '🔄 Сбросить трафик'),
|
||
callback_data='subscription_reset_traffic',
|
||
)
|
||
]
|
||
)
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('SWITCH_TRAFFIC_BUTTON', '🔄 Переключить трафик'),
|
||
callback_data='subscription_switch_traffic',
|
||
)
|
||
]
|
||
)
|
||
|
||
# Устройства: для тарифов - только если указана цена за устройство
|
||
if has_tariff:
|
||
tariff_device_price = getattr(tariff, 'device_price_kopeks', None)
|
||
if tariff_device_price is not None and tariff_device_price > 0:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CHANGE_DEVICES_BUTTON', '📱 Изменить устройства'),
|
||
callback_data='subscription_change_devices',
|
||
)
|
||
]
|
||
)
|
||
elif settings.is_devices_selection_enabled():
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CHANGE_DEVICES_BUTTON', '📱 Изменить устройства'),
|
||
callback_data='subscription_change_devices',
|
||
)
|
||
]
|
||
)
|
||
|
||
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('MANAGE_DEVICES_BUTTON', '🔧 Управление устройствами'),
|
||
callback_data='subscription_manage_devices',
|
||
)
|
||
]
|
||
)
|
||
|
||
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_subscription')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_device_reset_confirm_keyboard(
|
||
device_info: str, device_index: int, page: int, language: str = DEFAULT_LANGUAGE
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('RESET_DEVICE_CONFIRM_BUTTON', '✅ Да, сбросить это устройство'),
|
||
callback_data=f'confirm_reset_device_{device_index}_{page}',
|
||
)
|
||
],
|
||
[InlineKeyboardButton(text=texts.CANCEL, callback_data=f'devices_page_{page}')],
|
||
]
|
||
)
|
||
|
||
|
||
def get_device_management_help_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('DEVICE_CONNECTION_HELP', '❓ Как подключить устройство заново?'),
|
||
callback_data='device_connection_help',
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('MANAGE_DEVICES_BUTTON', '🔧 Управление устройствами'),
|
||
callback_data='subscription_manage_devices',
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('BACK_TO_SUBSCRIPTION', '⬅️ К подписке'), callback_data='menu_subscription'
|
||
)
|
||
],
|
||
]
|
||
)
|
||
|
||
|
||
# ==================== TICKET KEYBOARDS ====================
|
||
|
||
|
||
def get_ticket_cancel_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CANCEL_TICKET_CREATION', '❌ Отменить создание тикета'),
|
||
callback_data='cancel_ticket_creation',
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
|
||
def get_my_tickets_keyboard(
|
||
tickets: list[dict],
|
||
current_page: int = 1,
|
||
total_pages: int = 1,
|
||
language: str = DEFAULT_LANGUAGE,
|
||
page_prefix: str = 'my_tickets_page_',
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
keyboard = []
|
||
|
||
for ticket in tickets:
|
||
status_emoji = ticket.get('status_emoji', '❓')
|
||
# Override status emoji for closed tickets in admin list
|
||
if ticket.get('is_closed', False):
|
||
status_emoji = '✅'
|
||
title = ticket.get('title', 'Без названия')[:25]
|
||
button_text = f'{status_emoji} #{ticket["id"]} {title}'
|
||
|
||
keyboard.append([InlineKeyboardButton(text=button_text, callback_data=f'view_ticket_{ticket["id"]}')])
|
||
|
||
# Пагинация
|
||
if total_pages > 1:
|
||
nav_row = []
|
||
|
||
if current_page > 1:
|
||
nav_row.append(
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAGINATION_PREV', '⬅️'), callback_data=f'{page_prefix}{current_page - 1}'
|
||
)
|
||
)
|
||
|
||
nav_row.append(InlineKeyboardButton(text=f'{current_page}/{total_pages}', callback_data='current_page'))
|
||
|
||
if current_page < total_pages:
|
||
nav_row.append(
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAGINATION_NEXT', '➡️'), callback_data=f'{page_prefix}{current_page + 1}'
|
||
)
|
||
)
|
||
|
||
keyboard.append(nav_row)
|
||
|
||
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_support')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_ticket_view_keyboard(
|
||
ticket_id: int, is_closed: bool = False, language: str = DEFAULT_LANGUAGE
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
keyboard = []
|
||
|
||
if not is_closed:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('REPLY_TO_TICKET', '💬 Ответить'), callback_data=f'reply_ticket_{ticket_id}'
|
||
)
|
||
]
|
||
)
|
||
|
||
if not is_closed:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CLOSE_TICKET', '🔒 Закрыть тикет'), callback_data=f'close_ticket_{ticket_id}'
|
||
)
|
||
]
|
||
)
|
||
|
||
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='my_tickets')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_ticket_reply_cancel_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CANCEL_REPLY', '❌ Отменить ответ'), callback_data='cancel_ticket_reply'
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
|
||
# ==================== ADMIN TICKET KEYBOARDS ====================
|
||
|
||
|
||
def get_admin_tickets_keyboard(
|
||
tickets: list[dict],
|
||
current_page: int = 1,
|
||
total_pages: int = 1,
|
||
language: str = DEFAULT_LANGUAGE,
|
||
scope: str = 'all',
|
||
*,
|
||
back_callback: str = 'admin_submenu_support',
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
keyboard = []
|
||
|
||
# Разделяем открытые/закрытые
|
||
open_rows = []
|
||
closed_rows = []
|
||
for ticket in tickets:
|
||
status_emoji = ticket.get('status_emoji', '❓')
|
||
if ticket.get('is_closed', False):
|
||
status_emoji = '✅'
|
||
user_name = ticket.get('user_name', 'Unknown')
|
||
username = ticket.get('username')
|
||
telegram_id = ticket.get('telegram_id')
|
||
# Сформируем компактное отображение: Имя (@username | ID)
|
||
name_parts = [user_name[:15]]
|
||
contact_parts = []
|
||
if username:
|
||
contact_parts.append(f'@{username}')
|
||
if telegram_id:
|
||
contact_parts.append(str(telegram_id))
|
||
if contact_parts:
|
||
name_parts.append(f'({" | ".join(contact_parts)})')
|
||
name_display = ' '.join(name_parts)
|
||
title = ticket.get('title', 'Без названия')[:20]
|
||
locked_emoji = ticket.get('locked_emoji', '')
|
||
button_text = f'{status_emoji} #{ticket["id"]} {locked_emoji} {name_display}: {title}'.replace(' ', ' ')
|
||
row = [InlineKeyboardButton(text=button_text, callback_data=f'admin_view_ticket_{ticket["id"]}')]
|
||
if ticket.get('is_closed', False):
|
||
closed_rows.append(row)
|
||
else:
|
||
open_rows.append(row)
|
||
|
||
# Scope switcher
|
||
switch_row = []
|
||
switch_row.append(
|
||
InlineKeyboardButton(text=texts.t('OPEN_TICKETS', '🔴 Открытые'), callback_data='admin_tickets_scope_open')
|
||
)
|
||
switch_row.append(
|
||
InlineKeyboardButton(text=texts.t('CLOSED_TICKETS', '🟢 Закрытые'), callback_data='admin_tickets_scope_closed')
|
||
)
|
||
keyboard.append(switch_row)
|
||
|
||
if open_rows and scope in ('all', 'open'):
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('ADMIN_CLOSE_ALL_OPEN_TICKETS', '🔒 Закрыть все открытые'),
|
||
callback_data='admin_tickets_close_all_open',
|
||
)
|
||
]
|
||
)
|
||
keyboard.append(
|
||
[InlineKeyboardButton(text=texts.t('OPEN_TICKETS_HEADER', 'Открытые тикеты'), callback_data='noop')]
|
||
)
|
||
keyboard.extend(open_rows)
|
||
if closed_rows and scope in ('all', 'closed'):
|
||
keyboard.append(
|
||
[InlineKeyboardButton(text=texts.t('CLOSED_TICKETS_HEADER', 'Закрытые тикеты'), callback_data='noop')]
|
||
)
|
||
keyboard.extend(closed_rows)
|
||
|
||
# Пагинация
|
||
if total_pages > 1:
|
||
nav_row = []
|
||
|
||
if current_page > 1:
|
||
nav_row.append(
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAGINATION_PREV', '⬅️'), callback_data=f'admin_tickets_page_{scope}_{current_page - 1}'
|
||
)
|
||
)
|
||
|
||
nav_row.append(InlineKeyboardButton(text=f'{current_page}/{total_pages}', callback_data='current_page'))
|
||
|
||
if current_page < total_pages:
|
||
nav_row.append(
|
||
InlineKeyboardButton(
|
||
text=texts.t('PAGINATION_NEXT', '➡️'), callback_data=f'admin_tickets_page_{scope}_{current_page + 1}'
|
||
)
|
||
)
|
||
|
||
keyboard.append(nav_row)
|
||
|
||
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data=back_callback)])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_admin_ticket_view_keyboard(
|
||
ticket_id: int, is_closed: bool = False, language: str = DEFAULT_LANGUAGE, *, is_user_blocked: bool = False
|
||
) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
keyboard = []
|
||
|
||
if not is_closed:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('REPLY_TO_TICKET', '💬 Ответить'), callback_data=f'admin_reply_ticket_{ticket_id}'
|
||
)
|
||
]
|
||
)
|
||
|
||
if not is_closed:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CLOSE_TICKET', '🔒 Закрыть тикет'), callback_data=f'admin_close_ticket_{ticket_id}'
|
||
)
|
||
]
|
||
)
|
||
|
||
# Блок-контролы: когда не заблокирован — показать два варианта, когда заблокирован — только "Разблокировать"
|
||
if is_user_blocked:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('UNBLOCK', '✅ Разблокировать'), callback_data=f'admin_unblock_user_ticket_{ticket_id}'
|
||
)
|
||
]
|
||
)
|
||
else:
|
||
keyboard.append(
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('BLOCK_FOREVER', '🚫 Заблокировать'),
|
||
callback_data=f'admin_block_user_perm_ticket_{ticket_id}',
|
||
),
|
||
InlineKeyboardButton(
|
||
text=texts.t('BLOCK_BY_TIME', '⏳ Блок по времени'),
|
||
callback_data=f'admin_block_user_ticket_{ticket_id}',
|
||
),
|
||
]
|
||
)
|
||
|
||
keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='admin_tickets')])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||
|
||
|
||
def get_admin_ticket_reply_cancel_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
return InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=texts.t('CANCEL_REPLY', '❌ Отменить ответ'), callback_data='cancel_admin_ticket_reply'
|
||
)
|
||
]
|
||
]
|
||
)
|