mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-21 20:01:47 +00:00
1160 lines
46 KiB
Python
1160 lines
46 KiB
Python
from aiogram import Router, F
|
||
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo
|
||
from aiogram.filters import Command, StateFilter
|
||
from aiogram.fsm.context import FSMContext
|
||
from aiogram.fsm.state import State, StatesGroup
|
||
from datetime import datetime, timedelta
|
||
import logging
|
||
import secrets
|
||
from typing import Optional, Dict, Any
|
||
|
||
from database import Database, User
|
||
from remnawave_api import RemnaWaveAPI
|
||
from keyboards import *
|
||
from translations import t
|
||
from utils import *
|
||
from config import Config
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# FSM States
|
||
class BotStates(StatesGroup):
|
||
waiting_language = State()
|
||
waiting_amount = State()
|
||
waiting_promocode = State()
|
||
|
||
# Admin states
|
||
admin_create_sub_name = State()
|
||
admin_create_sub_desc = State()
|
||
admin_create_sub_price = State()
|
||
admin_create_sub_days = State()
|
||
admin_create_sub_traffic = State()
|
||
admin_create_sub_squad = State()
|
||
|
||
admin_add_balance_user = State()
|
||
admin_add_balance_amount = State()
|
||
|
||
admin_create_promo_code = State()
|
||
admin_create_promo_discount = State()
|
||
admin_create_promo_limit = State()
|
||
admin_edit_sub_value = State()
|
||
admin_send_message_user = State()
|
||
admin_send_message_text = State()
|
||
admin_broadcast_text = State()
|
||
admin_create_sub_squad_select = State()
|
||
|
||
router = Router()
|
||
|
||
# Start command
|
||
@router.message(Command("start"))
|
||
async def start_command(message: Message, state: FSMContext, db: Database, **kwargs):
|
||
"""Handle /start command"""
|
||
user = kwargs.get('user')
|
||
config = kwargs.get('config')
|
||
|
||
# If user is None, try to create a minimal response
|
||
if not user:
|
||
logger.error(f"User is None for telegram_id {message.from_user.id}")
|
||
await message.answer("❌ Ошибка инициализации пользователя. Попробуйте позже.")
|
||
return
|
||
|
||
# Clear any existing state
|
||
await state.clear()
|
||
|
||
if not user.language or user.language == 'ru': # Default handling
|
||
await message.answer(
|
||
t('select_language'),
|
||
reply_markup=language_keyboard()
|
||
)
|
||
await state.set_state(BotStates.waiting_language)
|
||
else:
|
||
await show_main_menu(message, user.language, user.is_admin, user.telegram_id, db, config)
|
||
|
||
# Language selection
|
||
@router.callback_query(F.data.startswith("lang_"))
|
||
async def language_callback(callback: CallbackQuery, state: FSMContext, db: Database, **kwargs):
|
||
"""Handle language selection"""
|
||
user = kwargs.get('user')
|
||
config = kwargs.get('config')
|
||
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
lang = callback.data.split("_")[1]
|
||
|
||
# Update user language
|
||
try:
|
||
user.language = lang
|
||
await db.update_user(user)
|
||
|
||
# Check if this is initial language selection or language change
|
||
current_state = await state.get_state()
|
||
is_initial_setup = current_state == BotStates.waiting_language.state
|
||
|
||
if is_initial_setup:
|
||
await callback.message.edit_text(
|
||
t('language_selected', lang),
|
||
reply_markup=None
|
||
)
|
||
await show_main_menu(callback.message, lang, user.is_admin, user.telegram_id, db, config)
|
||
await state.clear()
|
||
else:
|
||
# This is a language change from main menu
|
||
# Проверяем, доступна ли тестовая подписка
|
||
show_trial = False
|
||
if config and config.TRIAL_ENABLED and db:
|
||
try:
|
||
has_used = await db.has_used_trial(user.telegram_id)
|
||
show_trial = not has_used
|
||
except Exception as e:
|
||
logger.error(f"Error checking trial availability: {e}")
|
||
|
||
await callback.message.edit_text(
|
||
t('language_changed', lang),
|
||
reply_markup=main_menu_keyboard(lang, user.is_admin, show_trial)
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error updating user language: {e}")
|
||
await callback.answer("❌ Ошибка обновления языка")
|
||
|
||
async def show_main_menu(message: Message, lang: str, is_admin: bool = False, user_id: int = None, db: Database = None, config: Config = None):
|
||
"""Show main menu"""
|
||
try:
|
||
show_trial = False
|
||
|
||
# Проверяем, доступна ли тестовая подписка
|
||
if config and config.TRIAL_ENABLED and user_id and db:
|
||
has_used = await db.has_used_trial(user_id)
|
||
show_trial = not has_used
|
||
|
||
await message.answer(
|
||
t('main_menu', lang),
|
||
reply_markup=main_menu_keyboard(lang, is_admin, show_trial)
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Error showing main menu: {e}")
|
||
await message.answer("❌ Ошибка отображения меню")
|
||
|
||
# Main menu handlers
|
||
@router.callback_query(F.data == "main_menu")
|
||
async def main_menu_callback(callback: CallbackQuery, **kwargs):
|
||
"""Return to main menu"""
|
||
user = kwargs.get('user')
|
||
db = kwargs.get('db')
|
||
config = kwargs.get('config')
|
||
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
show_trial = False
|
||
|
||
# Проверяем, доступна ли тестовая подписка
|
||
if config and config.TRIAL_ENABLED and db:
|
||
try:
|
||
has_used = await db.has_used_trial(user.telegram_id)
|
||
show_trial = not has_used
|
||
except Exception as e:
|
||
logger.error(f"Error checking trial availability: {e}")
|
||
|
||
await callback.message.edit_text(
|
||
t('main_menu', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin, show_trial)
|
||
)
|
||
|
||
@router.callback_query(F.data == "trial_subscription")
|
||
async def trial_subscription_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||
"""Show trial subscription info"""
|
||
user = kwargs.get('user')
|
||
config = kwargs.get('config')
|
||
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
if not config or not config.TRIAL_ENABLED:
|
||
await callback.answer(t('trial_not_available', user.language))
|
||
return
|
||
|
||
try:
|
||
# Проверяем, не использовал ли пользователь уже тестовую подписку
|
||
has_used = await db.has_used_trial(user.telegram_id)
|
||
if has_used:
|
||
await callback.answer(t('trial_already_used', user.language))
|
||
return
|
||
|
||
text = t('trial_info', user.language,
|
||
days=config.TRIAL_DURATION_DAYS,
|
||
traffic=config.TRIAL_TRAFFIC_GB
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=trial_subscription_keyboard(user.language)
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Error showing trial info: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
@router.callback_query(F.data == "confirm_trial")
|
||
async def confirm_trial_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||
"""Confirm and create trial subscription"""
|
||
user = kwargs.get('user')
|
||
api = kwargs.get('api')
|
||
config = kwargs.get('config')
|
||
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
if not config or not config.TRIAL_ENABLED:
|
||
await callback.answer(t('trial_not_available', user.language))
|
||
return
|
||
|
||
try:
|
||
# Проверяем еще раз, не использовал ли пользователь тестовую подписку
|
||
has_used = await db.has_used_trial(user.telegram_id)
|
||
if has_used:
|
||
await callback.answer(t('trial_already_used', user.language))
|
||
return
|
||
|
||
if not api:
|
||
logger.error("API not available in kwargs")
|
||
await callback.message.edit_text(
|
||
t('trial_error', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
return
|
||
|
||
# Создаем пользователя в RemnaWave для тестовой подписки
|
||
username = generate_username()
|
||
password = generate_password()
|
||
|
||
logger.info(f"Creating trial subscription for user {user.telegram_id}")
|
||
|
||
remna_user = await api.create_user(
|
||
username=username,
|
||
password=password,
|
||
traffic_limit=config.TRIAL_TRAFFIC_GB * 1024 * 1024 * 1024,
|
||
expiry_time=calculate_expiry_date(config.TRIAL_DURATION_DAYS),
|
||
telegram_id=user.telegram_id,
|
||
activeInternalSquads=[config.TRIAL_SQUAD_UUID]
|
||
)
|
||
|
||
# Обрабатываем ответ API
|
||
if remna_user:
|
||
if 'data' in remna_user and 'uuid' in remna_user['data']:
|
||
user_uuid = remna_user['data']['uuid']
|
||
short_uuid = remna_user['data'].get('shortUuid')
|
||
elif 'response' in remna_user and 'uuid' in remna_user['response']:
|
||
user_uuid = remna_user['response']['uuid']
|
||
short_uuid = remna_user['response'].get('shortUuid')
|
||
else:
|
||
logger.error(f"Invalid API response structure: {remna_user}")
|
||
await callback.message.edit_text(
|
||
t('trial_error', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
return
|
||
|
||
if user_uuid:
|
||
# Если shortUuid не получен, запрашиваем его отдельно
|
||
if not short_uuid:
|
||
user_details = await api.get_user_by_uuid(user_uuid)
|
||
if user_details and 'shortUuid' in user_details:
|
||
short_uuid = user_details['shortUuid']
|
||
|
||
if not short_uuid:
|
||
logger.error(f"Failed to get shortUuid for trial user")
|
||
await callback.message.edit_text(
|
||
t('trial_error', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
return
|
||
|
||
logger.info(f"Created trial user with UUID: {user_uuid}, shortUuid: {short_uuid}")
|
||
else:
|
||
logger.error("Failed to create trial user in RemnaWave")
|
||
await callback.message.edit_text(
|
||
t('trial_error', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
return
|
||
else:
|
||
logger.error("Failed to create trial user in RemnaWave API")
|
||
await callback.message.edit_text(
|
||
t('trial_error', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
return
|
||
|
||
# Создаем временную тестовую подписку, которая НЕ будет отображаться в админке
|
||
trial_subscription = await db.create_subscription(
|
||
name=f"Trial_{user.telegram_id}_{int(datetime.utcnow().timestamp())}", # Уникальное имя
|
||
description="Автоматически созданная тестовая подписка",
|
||
price=0,
|
||
duration_days=config.TRIAL_DURATION_DAYS,
|
||
traffic_limit_gb=config.TRIAL_TRAFFIC_GB,
|
||
squad_uuid=config.TRIAL_SQUAD_UUID
|
||
)
|
||
|
||
# Помечаем подписку как тестовую И неактивную для админки
|
||
trial_subscription.is_trial = True
|
||
trial_subscription.is_active = False # Скрываем от обычных запросов
|
||
await db.update_subscription(trial_subscription)
|
||
|
||
# Создаем пользовательскую подписку
|
||
expires_at = datetime.utcnow() + timedelta(days=config.TRIAL_DURATION_DAYS)
|
||
|
||
await db.create_user_subscription(
|
||
user_id=user.telegram_id,
|
||
subscription_id=trial_subscription.id,
|
||
short_uuid=short_uuid,
|
||
expires_at=expires_at
|
||
)
|
||
|
||
# Помечаем, что пользователь использовал тестовую подписку
|
||
await db.mark_trial_used(user.telegram_id)
|
||
|
||
# Создаем запись о платеже (бесплатном)
|
||
await db.create_payment(
|
||
user_id=user.telegram_id,
|
||
amount=0,
|
||
payment_type='trial',
|
||
description='Активация тестовой подписки',
|
||
status='completed'
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
t('trial_success', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
|
||
log_user_action(user.telegram_id, "trial_subscription_activated", "Free trial")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error creating trial subscription: {e}")
|
||
await callback.message.edit_text(
|
||
t('trial_error', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
|
||
@router.callback_query(F.data == "change_language")
|
||
async def change_language_callback(callback: CallbackQuery, **kwargs):
|
||
"""Show language selection for changing language"""
|
||
user = kwargs.get('user')
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
t('select_language'),
|
||
reply_markup=language_keyboard()
|
||
)
|
||
|
||
@router.callback_query(F.data == "balance")
|
||
async def balance_callback(callback: CallbackQuery, **kwargs):
|
||
"""Show balance menu"""
|
||
user = kwargs.get('user')
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
text = t('your_balance', user.language, balance=user.balance)
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=balance_keyboard(user.language)
|
||
)
|
||
|
||
@router.callback_query(F.data == "topup_balance")
|
||
async def topup_balance_callback(callback: CallbackQuery, **kwargs):
|
||
"""Show top up options"""
|
||
user = kwargs.get('user')
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
t('topup_balance', user.language),
|
||
reply_markup=topup_keyboard(user.language)
|
||
)
|
||
|
||
@router.callback_query(F.data == "topup_card")
|
||
async def topup_card_callback(callback: CallbackQuery, **kwargs):
|
||
"""Handle card payment"""
|
||
user = kwargs.get('user')
|
||
config = kwargs.get('config')
|
||
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
support_username = config.SUPPORT_USERNAME if config else 'support'
|
||
text = t('payment_card_info', user.language, support=support_username)
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=back_keyboard("topup_balance", user.language)
|
||
)
|
||
|
||
@router.callback_query(F.data == "topup_support")
|
||
async def topup_support_callback(callback: CallbackQuery, state: FSMContext, **kwargs):
|
||
"""Handle support payment"""
|
||
user = kwargs.get('user')
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
t('enter_amount', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.waiting_amount)
|
||
|
||
@router.message(StateFilter(BotStates.waiting_amount))
|
||
async def handle_amount(message: Message, state: FSMContext, db: Database, **kwargs):
|
||
"""Handle amount input"""
|
||
user = kwargs.get('user')
|
||
config = kwargs.get('config')
|
||
|
||
if not user:
|
||
await message.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
is_valid, amount = is_valid_amount(message.text)
|
||
|
||
if not is_valid:
|
||
await message.answer(t('invalid_amount', user.language))
|
||
return
|
||
|
||
try:
|
||
# Create payment record
|
||
payment = await db.create_payment(
|
||
user_id=user.telegram_id,
|
||
amount=amount,
|
||
payment_type='topup',
|
||
description=f'Пополнение баланса на {amount} руб.'
|
||
)
|
||
|
||
support_username = config.SUPPORT_USERNAME if config else 'support'
|
||
|
||
# Уведомляем админов о запросе на пополнение
|
||
if config and config.ADMIN_IDS:
|
||
admin_text = f"💰 Новый запрос на пополнение!\n\n"
|
||
admin_text += f"👤 Пользователь: {user.first_name or 'N/A'} (@{user.username or 'N/A'})\n"
|
||
admin_text += f"🆔 ID: {user.telegram_id}\n"
|
||
admin_text += f"💵 Сумма: {amount} руб.\n"
|
||
admin_text += f"📝 ID платежа: {payment.id}"
|
||
|
||
# Отправляем уведомление всем админам
|
||
from aiogram import Bot
|
||
bot = kwargs.get('bot')
|
||
if bot:
|
||
for admin_id in config.ADMIN_IDS:
|
||
try:
|
||
await bot.send_message(
|
||
admin_id,
|
||
admin_text,
|
||
reply_markup=admin_payment_keyboard(payment.id, user.language)
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Failed to notify admin {admin_id}: {e}")
|
||
|
||
text = t('payment_created', user.language, support=support_username)
|
||
await message.answer(
|
||
text,
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
await state.clear()
|
||
except Exception as e:
|
||
logger.error(f"Error creating payment: {e}")
|
||
await message.answer(
|
||
t('error_occurred', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
await state.clear()
|
||
|
||
@router.callback_query(F.data == "payment_history")
|
||
async def payment_history_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||
"""Show payment history"""
|
||
user = kwargs.get('user')
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
try:
|
||
payments = await db.get_user_payments(user.telegram_id)
|
||
|
||
if not payments:
|
||
text = t('no_payments', user.language)
|
||
else:
|
||
text = "📊 " + t('payment_history', user.language) + ":\n\n"
|
||
for payment in payments[:10]: # Show last 10 payments
|
||
date_str = format_datetime(payment.created_at, user.language)
|
||
status = format_payment_status(payment.status, user.language)
|
||
text += t('payment_item', user.language,
|
||
date=date_str,
|
||
amount=payment.amount,
|
||
description=payment.description,
|
||
status=status
|
||
) + "\n"
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=back_keyboard("balance", user.language)
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Error getting payment history: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
# Subscriptions
|
||
@router.callback_query(F.data == "buy_subscription")
|
||
async def buy_subscription_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||
"""Show available subscriptions (excluding trial)"""
|
||
user = kwargs.get('user')
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
try:
|
||
subscriptions = await db.get_all_subscriptions(exclude_trial=True)
|
||
|
||
if not subscriptions:
|
||
await callback.message.edit_text(
|
||
"❌ Нет доступных подписок",
|
||
reply_markup=back_keyboard("main_menu", user.language)
|
||
)
|
||
return
|
||
|
||
# Convert to dict format
|
||
sub_list = []
|
||
for sub in subscriptions:
|
||
sub_list.append({
|
||
'id': sub.id,
|
||
'name': sub.name,
|
||
'price': sub.price
|
||
})
|
||
|
||
await callback.message.edit_text(
|
||
t('buy_subscription', user.language),
|
||
reply_markup=subscriptions_keyboard(sub_list, user.language)
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Error getting subscriptions: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
@router.callback_query(F.data.startswith("buy_sub_"))
|
||
async def buy_subscription_detail(callback: CallbackQuery, db: Database, **kwargs):
|
||
"""Show subscription details"""
|
||
user = kwargs.get('user')
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
try:
|
||
sub_id = int(callback.data.split("_")[2])
|
||
subscription = await db.get_subscription_by_id(sub_id)
|
||
|
||
if not subscription:
|
||
await callback.answer("❌ Подписка не найдена")
|
||
return
|
||
|
||
sub_dict = {
|
||
'name': subscription.name,
|
||
'price': subscription.price,
|
||
'duration_days': subscription.duration_days,
|
||
'traffic_limit_gb': subscription.traffic_limit_gb,
|
||
'description': subscription.description or ''
|
||
}
|
||
|
||
text = format_subscription_info(sub_dict, user.language)
|
||
|
||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||
[InlineKeyboardButton(
|
||
text=t('buy_subscription_btn', user.language, price=subscription.price),
|
||
callback_data=f"confirm_buy_{sub_id}"
|
||
)],
|
||
[InlineKeyboardButton(text=t('back', user.language), callback_data="buy_subscription")]
|
||
])
|
||
|
||
await callback.message.edit_text(text, reply_markup=keyboard)
|
||
except Exception as e:
|
||
logger.error(f"Error showing subscription detail: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
@router.callback_query(F.data.startswith("confirm_buy_"))
|
||
async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
|
||
"""Confirm subscription purchase"""
|
||
user = kwargs.get('user')
|
||
api = kwargs.get('api')
|
||
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
try:
|
||
sub_id = int(callback.data.split("_")[2])
|
||
subscription = await db.get_subscription_by_id(sub_id)
|
||
|
||
if not subscription:
|
||
await callback.answer("❌ Подписка не найдена")
|
||
return
|
||
|
||
# Check balance
|
||
if user.balance < subscription.price:
|
||
await callback.answer(t('insufficient_balance', user.language))
|
||
return
|
||
|
||
# Get API from kwargs
|
||
if not api:
|
||
logger.error("API not available in kwargs")
|
||
await callback.message.edit_text(
|
||
t('purchase_error', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
return
|
||
|
||
# ИСПРАВЛЕНИЕ: Всегда создавать НОВЫЙ пользователь в RemnaWave для каждой подписки
|
||
# Это позволит избежать дублирования short_uuid и даст каждой подписке уникальную ссылку
|
||
|
||
username = generate_username()
|
||
password = generate_password()
|
||
|
||
logger.info(f"Creating new RemnaWave user for subscription {subscription.name}")
|
||
|
||
remna_user = await api.create_user(
|
||
username=username,
|
||
password=password,
|
||
traffic_limit=subscription.traffic_limit_gb * 1024 * 1024 * 1024 if subscription.traffic_limit_gb > 0 else 0,
|
||
expiry_time=calculate_expiry_date(subscription.duration_days),
|
||
telegram_id=user.telegram_id,
|
||
activeInternalSquads=[subscription.squad_uuid]
|
||
)
|
||
|
||
# Handle API response which may use 'data' or 'response' key
|
||
if remna_user:
|
||
if 'data' in remna_user and 'uuid' in remna_user['data']:
|
||
user_uuid = remna_user['data']['uuid']
|
||
short_uuid = remna_user['data'].get('shortUuid')
|
||
elif 'response' in remna_user and 'uuid' in remna_user['response']:
|
||
user_uuid = remna_user['response']['uuid']
|
||
short_uuid = remna_user['response'].get('shortUuid')
|
||
else:
|
||
logger.error(f"Invalid API response structure: {remna_user}")
|
||
await callback.message.edit_text(
|
||
"❌ Ошибка создания пользователя в системе",
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
return
|
||
|
||
if user_uuid:
|
||
# Обновляем основного пользователя только если у него еще нет remnawave_uuid
|
||
if not user.remnawave_uuid:
|
||
user.remnawave_uuid = user_uuid
|
||
await db.update_user(user)
|
||
|
||
# Если shortUuid не получен, запрашиваем его отдельно
|
||
if not short_uuid:
|
||
user_details = await api.get_user_by_uuid(user_uuid)
|
||
if user_details and 'shortUuid' in user_details:
|
||
short_uuid = user_details['shortUuid']
|
||
|
||
if not short_uuid:
|
||
logger.error(f"Failed to get shortUuid for new user")
|
||
await callback.message.edit_text(
|
||
"❌ Ошибка получения данных подписки",
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
return
|
||
|
||
logger.info(f"Created new user with UUID: {user_uuid}, shortUuid: {short_uuid}")
|
||
else:
|
||
logger.error("Failed to create user in RemnaWave")
|
||
await callback.message.edit_text(
|
||
"❌ Ошибка создания пользователя",
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
return
|
||
else:
|
||
logger.error("Failed to create user in RemnaWave API")
|
||
await callback.message.edit_text(
|
||
"❌ Ошибка создания пользователя в системе",
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
return
|
||
|
||
# Deduct balance
|
||
user.balance -= subscription.price
|
||
await db.update_user(user)
|
||
|
||
# Create user subscription record with the NEW short_uuid
|
||
expires_at = datetime.utcnow() + timedelta(days=subscription.duration_days)
|
||
|
||
await db.create_user_subscription(
|
||
user_id=user.telegram_id,
|
||
subscription_id=subscription.id,
|
||
short_uuid=short_uuid, # Это НОВЫЙ уникальный shortUuid
|
||
expires_at=expires_at
|
||
)
|
||
|
||
# Create payment record
|
||
await db.create_payment(
|
||
user_id=user.telegram_id,
|
||
amount=-subscription.price,
|
||
payment_type='subscription',
|
||
description=f'Покупка подписки: {subscription.name}',
|
||
status='completed'
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
t('subscription_purchased', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
|
||
log_user_action(user.telegram_id, "subscription_purchased", f"Sub: {subscription.name}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error purchasing subscription: {e}")
|
||
await callback.message.edit_text(
|
||
t('purchase_error', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
|
||
@router.callback_query(F.data == "my_subscriptions")
|
||
async def my_subscriptions_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||
"""Show user's subscriptions"""
|
||
user = kwargs.get('user')
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
try:
|
||
user_subs = await db.get_user_subscriptions(user.telegram_id)
|
||
|
||
if not user_subs:
|
||
await callback.message.edit_text(
|
||
t('no_subscriptions', user.language),
|
||
reply_markup=back_keyboard("main_menu", user.language)
|
||
)
|
||
return
|
||
|
||
# Get subscription details
|
||
sub_list = []
|
||
for user_sub in user_subs:
|
||
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
|
||
if subscription:
|
||
sub_list.append({
|
||
'id': user_sub.id,
|
||
'name': subscription.name
|
||
})
|
||
|
||
await callback.message.edit_text(
|
||
t('my_subscriptions', user.language),
|
||
reply_markup=user_subscriptions_keyboard(sub_list, user.language)
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Error getting user subscriptions: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
@router.callback_query(F.data.startswith("view_sub_"))
|
||
async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwargs):
|
||
"""View subscription details"""
|
||
user = kwargs.get('user')
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
try:
|
||
user_sub_id = int(callback.data.split("_")[2])
|
||
|
||
# Get user subscription
|
||
user_subs = await db.get_user_subscriptions(user.telegram_id)
|
||
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
|
||
|
||
if not user_sub:
|
||
await callback.answer("❌ Подписка не найдена")
|
||
return
|
||
|
||
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
|
||
if not subscription:
|
||
await callback.answer("❌ Подписка не найдена")
|
||
return
|
||
|
||
sub_dict = {
|
||
'name': subscription.name,
|
||
'duration_days': subscription.duration_days,
|
||
'traffic_limit_gb': subscription.traffic_limit_gb,
|
||
'description': subscription.description or ''
|
||
}
|
||
|
||
# Check if subscription is expiring soon (within 3 days)
|
||
from datetime import datetime, timedelta
|
||
now = datetime.utcnow()
|
||
days_until_expiry = (user_sub.expires_at - now).days
|
||
|
||
# ИСПРАВЛЕНИЕ: Запрещаем продление тестовых подписок
|
||
show_extend = (0 <= days_until_expiry <= 3 and
|
||
user_sub.is_active and
|
||
not subscription.is_trial) # Добавили проверку на тестовую подписку
|
||
|
||
text = format_user_subscription_info(user_sub.__dict__, sub_dict, user_sub.expires_at, user.language)
|
||
|
||
# Add expiry warning if subscription expires soon (but don't show for trial)
|
||
if show_extend:
|
||
text += f"\n\n⚠️ {t('subscription_expires_soon', user.language, days=days_until_expiry)}"
|
||
elif subscription.is_trial and 0 <= days_until_expiry <= 3:
|
||
# Специальное сообщение для тестовых подписок
|
||
text += f"\n\n ℹ️ Тестовая подписка истекает через {days_until_expiry} дн. Продление недоступно."
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=user_subscription_detail_keyboard(user_sub_id, user.language, show_extend)
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Error viewing subscription detail: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
@router.callback_query(F.data.startswith("extend_sub_"))
|
||
async def extend_subscription_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||
"""Show extend subscription confirmation"""
|
||
user = kwargs.get('user')
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
try:
|
||
user_sub_id = int(callback.data.split("_")[2])
|
||
|
||
# Get user subscription
|
||
user_subs = await db.get_user_subscriptions(user.telegram_id)
|
||
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
|
||
|
||
if not user_sub:
|
||
await callback.answer("❌ Подписка не найдена")
|
||
return
|
||
|
||
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
|
||
if not subscription:
|
||
await callback.answer("❌ Подписка не найдена")
|
||
return
|
||
|
||
# ИСПРАВЛЕНИЕ: Запрещаем продление тестовых подписок
|
||
if subscription.is_trial:
|
||
await callback.answer("❌ Тестовую подписку нельзя продлить")
|
||
return
|
||
|
||
# Check balance
|
||
if user.balance < subscription.price:
|
||
await callback.answer(t('insufficient_balance', user.language))
|
||
return
|
||
|
||
text = t('extend_confirmation', user.language,
|
||
name=subscription.name,
|
||
days=subscription.duration_days,
|
||
price=subscription.price
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=extend_subscription_keyboard(user_sub_id, user.language)
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Error showing extend confirmation: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
@router.callback_query(F.data.startswith("confirm_extend_"))
|
||
async def confirm_extend_subscription(callback: CallbackQuery, db: Database, **kwargs):
|
||
"""Confirm subscription extension"""
|
||
user = kwargs.get('user')
|
||
api = kwargs.get('api')
|
||
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
try:
|
||
user_sub_id = int(callback.data.split("_")[2])
|
||
|
||
# Get user subscription
|
||
user_subs = await db.get_user_subscriptions(user.telegram_id)
|
||
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
|
||
|
||
if not user_sub:
|
||
await callback.answer("❌ Подписка не найдена")
|
||
return
|
||
|
||
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
|
||
if not subscription:
|
||
await callback.answer("❌ Подписка не найдена")
|
||
return
|
||
|
||
# ИСПРАВЛЕНИЕ: Дополнительная проверка на тестовую подписку
|
||
if subscription.is_trial:
|
||
await callback.answer("❌ Тестовую подписку нельзя продлить")
|
||
return
|
||
|
||
# Check balance again
|
||
if user.balance < subscription.price:
|
||
await callback.answer(t('insufficient_balance', user.language))
|
||
return
|
||
|
||
# Calculate new expiry date
|
||
from datetime import timedelta
|
||
new_expiry = user_sub.expires_at + timedelta(days=subscription.duration_days)
|
||
|
||
# Update subscription in RemnaWave if API is available
|
||
if api and user_sub.short_uuid:
|
||
try:
|
||
# Получаем информацию о пользователе по short_uuid
|
||
logger.info(f"Updating RemnaWave subscription for shortUuid: {user_sub.short_uuid}")
|
||
|
||
# Сначала получаем полную информацию о пользователе
|
||
remna_user_details = await api.get_user_by_short_uuid(user_sub.short_uuid)
|
||
if remna_user_details:
|
||
user_uuid = remna_user_details.get('uuid')
|
||
if user_uuid:
|
||
# Обновляем пользователя в RemnaWave с новой датой истечения
|
||
update_data = {
|
||
'expireAt': new_expiry.isoformat() + 'Z'
|
||
}
|
||
|
||
logger.info(f"Updating user {user_uuid} with new expiry: {update_data['expireAt']}")
|
||
result = await api.update_user(user_uuid, update_data)
|
||
|
||
if result:
|
||
logger.info(f"Successfully updated RemnaWave user expiry")
|
||
else:
|
||
logger.warning(f"Failed to update user in RemnaWave")
|
||
else:
|
||
logger.warning(f"Could not get user UUID from RemnaWave response")
|
||
else:
|
||
logger.warning(f"Could not find user in RemnaWave with shortUuid: {user_sub.short_uuid}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Failed to update expiry in RemnaWave: {e}")
|
||
# Продолжаем выполнение даже если обновление в RemnaWave не удалось
|
||
|
||
# Update local database
|
||
user_sub.expires_at = new_expiry
|
||
await db.update_user_subscription(user_sub)
|
||
|
||
# Deduct balance
|
||
user.balance -= subscription.price
|
||
await db.update_user(user)
|
||
|
||
# Create payment record
|
||
await db.create_payment(
|
||
user_id=user.telegram_id,
|
||
amount=-subscription.price,
|
||
payment_type='subscription_extend',
|
||
description=f'Продление подписки: {subscription.name}',
|
||
status='completed'
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
t('subscription_extended', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
|
||
log_user_action(user.telegram_id, "subscription_extended", f"Sub: {subscription.name}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error extending subscription: {e}")
|
||
await callback.message.edit_text(
|
||
t('error_occurred', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
|
||
@router.callback_query(F.data.startswith("get_connection_"))
|
||
async def get_connection_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||
"""Get connection link"""
|
||
user = kwargs.get('user')
|
||
api = kwargs.get('api')
|
||
config = kwargs.get('config')
|
||
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
try:
|
||
user_subs = await db.get_user_subscriptions(user.telegram_id)
|
||
|
||
sub_id = int(callback.data.split("_")[2])
|
||
user_sub = next((s for s in user_subs if s.id == sub_id), None)
|
||
if not user_sub:
|
||
await callback.answer("❌ Подписка не найдена")
|
||
return
|
||
|
||
# Используем shortUuid напрямую
|
||
short_uuid = user_sub.short_uuid
|
||
|
||
# ИСПРАВЛЕНИЕ: Приоритет config.SUBSCRIPTION_BASE_URL
|
||
if config and config.SUBSCRIPTION_BASE_URL:
|
||
connection_url = f"{config.SUBSCRIPTION_BASE_URL.rstrip('/')}/sub/{short_uuid}"
|
||
logger.info(f"Using config SUBSCRIPTION_BASE_URL: {connection_url}")
|
||
elif api:
|
||
# Если конфиг недоступен, используем API
|
||
connection_url = await api.get_subscription_url(short_uuid)
|
||
logger.info(f"Using API subscription URL: {connection_url}")
|
||
else:
|
||
# Последний fallback
|
||
connection_url = f"{config.SUBSCRIPTION_BASE_URL.rstrip('/')}/sub/{short_uuid}"
|
||
logger.warning(f"Using hardcoded fallback URL: {connection_url}")
|
||
|
||
text = t('connection_link', user.language, link=connection_url)
|
||
|
||
# Создаем клавиатуру с кнопкой подключения
|
||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||
[InlineKeyboardButton(text="🔗 Подключиться", web_app=WebAppInfo(url=connection_url))],
|
||
[InlineKeyboardButton(text=t('back', user.language), callback_data="my_subscriptions")]
|
||
])
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=keyboard,
|
||
parse_mode='Markdown'
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Error getting connection link: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
@router.callback_query(F.data.startswith("connect_sub_"))
|
||
async def connect_subscription_callback(callback: CallbackQuery, db: Database, api: RemnaWaveAPI, **kwargs):
|
||
"""Handle subscription connection button"""
|
||
user = kwargs.get('user')
|
||
config = kwargs.get('config')
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
try:
|
||
user_subs = await db.get_user_subscriptions(user.telegram_id)
|
||
|
||
sub_id = int(callback.data.split("_")[2])
|
||
user_sub = next((s for s in user_subs if s.id == sub_id), None)
|
||
if not user_sub:
|
||
await callback.answer("❌ Подписка не найдена")
|
||
return
|
||
|
||
# Получаем ссылку из RemnaWave API
|
||
connection_url = await api.get_subscription_by_short_uuid(user_sub.short_uuid)
|
||
|
||
if not connection_url:
|
||
# Используем config.SUBSCRIPTION_BASE_URL
|
||
subscription_base_url = config.SUBSCRIPTION_BASE_URL if config else "http://localhost"
|
||
connection_url = f"{subscription_base_url.rstrip('/')}/sub/{user_sub.short_uuid}"
|
||
|
||
# Открываем ссылку через URL кнопку
|
||
await callback.answer(url=connection_url)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error connecting to subscription: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
# Support
|
||
@router.callback_query(F.data == "support")
|
||
async def support_callback(callback: CallbackQuery, **kwargs):
|
||
"""Show support info"""
|
||
user = kwargs.get('user')
|
||
config = kwargs.get('config')
|
||
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
support_username = config.SUPPORT_USERNAME if config else 'support'
|
||
|
||
text = t('support_message', user.language, support=support_username)
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=back_keyboard("main_menu", user.language)
|
||
)
|
||
|
||
# Promocode
|
||
@router.callback_query(F.data == "promocode")
|
||
async def promocode_callback(callback: CallbackQuery, state: FSMContext, **kwargs):
|
||
"""Handle promocode input"""
|
||
user = kwargs.get('user')
|
||
if not user:
|
||
await callback.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
t('enter_promocode', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.waiting_promocode)
|
||
|
||
@router.message(StateFilter(BotStates.waiting_promocode))
|
||
async def handle_promocode(message: Message, state: FSMContext, db: Database, **kwargs):
|
||
"""Handle promocode input"""
|
||
user = kwargs.get('user')
|
||
if not user:
|
||
await message.answer("❌ Ошибка пользователя")
|
||
return
|
||
|
||
code = message.text.strip().upper()
|
||
|
||
if not validate_promocode_format(code):
|
||
await message.answer(t('invalid_input', user.language))
|
||
return
|
||
|
||
try:
|
||
promocode = await db.get_promocode_by_code(code)
|
||
|
||
if not promocode:
|
||
await message.answer(t('promocode_not_found', user.language))
|
||
return
|
||
|
||
# Check if promocode is active
|
||
if not promocode.is_active:
|
||
await message.answer(t('promocode_not_found', user.language))
|
||
return
|
||
|
||
# Check expiry
|
||
if promocode.expires_at and promocode.expires_at < datetime.utcnow():
|
||
await message.answer(t('promocode_expired', user.language))
|
||
return
|
||
|
||
# Check usage limit
|
||
if promocode.used_count >= promocode.usage_limit:
|
||
await message.answer(t('promocode_limit', user.language))
|
||
return
|
||
|
||
# Check if user already used this promocode
|
||
success = await db.use_promocode(user.telegram_id, promocode)
|
||
|
||
if not success:
|
||
await message.answer(t('promocode_used', user.language))
|
||
return
|
||
|
||
# Add to balance
|
||
await db.add_balance(user.telegram_id, promocode.discount_amount)
|
||
|
||
# Create payment record
|
||
await db.create_payment(
|
||
user_id=user.telegram_id,
|
||
amount=promocode.discount_amount,
|
||
payment_type='promocode',
|
||
description=f'Промокод: {code}',
|
||
status='completed'
|
||
)
|
||
|
||
discount_text = f"{promocode.discount_amount} руб."
|
||
await message.answer(
|
||
t('promocode_success', user.language, discount=discount_text),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
|
||
await state.clear()
|
||
log_user_action(user.telegram_id, "promocode_used", code)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error handling promocode: {e}")
|
||
await message.answer(
|
||
t('error_occurred', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
await state.clear()
|