import asyncio from aiogram import Router, F from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton from aiogram.filters import StateFilter from aiogram.fsm.context import FSMContext from datetime import datetime, timedelta import logging from typing import List, Dict from database import Database, User from remnawave_api import RemnaWaveAPI from keyboards import * from translations import t from utils import * from handlers import BotStates logger = logging.getLogger(__name__) admin_router = Router() # Admin panel access check async def check_admin_access(callback: CallbackQuery, user: User) -> bool: """Check if user has admin access""" if not user.is_admin: await callback.answer(t('not_admin', user.language)) return False return True # Admin panel main menu @admin_router.callback_query(F.data == "admin_panel") async def admin_panel_callback(callback: CallbackQuery, user: User, **kwargs): """Show admin panel""" if not await check_admin_access(callback, user): return await callback.message.edit_text( t('admin_menu', user.language), reply_markup=admin_menu_keyboard(user.language) ) # Statistics @admin_router.callback_query(F.data == "admin_stats") async def admin_stats_callback(callback: CallbackQuery, user: User, db: Database, api: RemnaWaveAPI = None, **kwargs): """Show statistics""" if not await check_admin_access(callback, user): return try: # Get database stats db_stats = await db.get_stats() # Get RemnaWave system stats (optional) system_stats = None nodes_stats = None if api: try: system_stats = await api.get_system_stats() nodes_stats = await api.get_nodes_statistics() except Exception as e: logger.warning(f"Failed to get RemnaWave stats: {e}") text = t('stats_info', user.language, users=db_stats['total_users'], subscriptions=db_stats['total_subscriptions_non_trial'], # Изменено revenue=db_stats['total_revenue'] ) if system_stats: text += "\n\n🖥 Системная статистика:" if 'data' in system_stats: data = system_stats['data'] if 'bandwidth' in data: bandwidth = data['bandwidth'] text += f"\n📊 Трафик: ↓{format_bytes(bandwidth.get('downlink', 0))} ↑{format_bytes(bandwidth.get('uplink', 0))}" if nodes_stats and 'data' in nodes_stats: nodes = nodes_stats['data'] online_nodes = len([n for n in nodes if n.get('status') == 'online']) text += f"\n🖥 Нод: {online_nodes}/{len(nodes)} онлайн" await callback.message.edit_text( text, reply_markup=back_keyboard("admin_panel", user.language) ) except Exception as e: logger.error(f"Error getting statistics: {e}") await callback.message.edit_text( t('error_occurred', user.language), reply_markup=back_keyboard("admin_panel", user.language) ) # Subscription management @admin_router.callback_query(F.data == "admin_subscriptions") async def admin_subscriptions_callback(callback: CallbackQuery, user: User, **kwargs): """Show subscription management""" if not await check_admin_access(callback, user): return await callback.message.edit_text( t('manage_subscriptions', user.language), reply_markup=admin_subscriptions_keyboard(user.language) ) @admin_router.callback_query(F.data == "create_subscription") async def create_subscription_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs): """Start subscription creation""" if not await check_admin_access(callback, user): return await callback.message.edit_text( t('enter_sub_name', user.language), reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_create_sub_name) @admin_router.message(StateFilter(BotStates.admin_create_sub_name)) async def handle_sub_name(message: Message, state: FSMContext, user: User, **kwargs): """Handle subscription name input""" name = message.text.strip() if len(name) < 3 or len(name) > 100: await message.answer("❌ Название должно быть от 3 до 100 символов") return await state.update_data(name=name) await message.answer( t('enter_sub_description', user.language), reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_create_sub_desc) @admin_router.message(StateFilter(BotStates.admin_create_sub_desc)) async def handle_sub_description(message: Message, state: FSMContext, user: User, **kwargs): """Handle subscription description input""" description = message.text.strip() if len(description) > 500: await message.answer("❌ Описание не должно превышать 500 символов") return await state.update_data(description=description) await message.answer( t('enter_sub_price', user.language), reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_create_sub_price) @admin_router.message(StateFilter(BotStates.admin_create_sub_price)) async def handle_sub_price(message: Message, state: FSMContext, user: User, **kwargs): """Handle subscription price input""" is_valid, price = is_valid_amount(message.text) if not is_valid: await message.answer(t('invalid_amount', user.language)) return await state.update_data(price=price) await message.answer( t('enter_sub_days', user.language), reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_create_sub_days) @admin_router.message(StateFilter(BotStates.admin_create_sub_days)) async def handle_sub_days(message: Message, state: FSMContext, user: User, **kwargs): """Handle subscription duration input""" try: days = int(message.text.strip()) if days <= 0 or days > 365: await message.answer("❌ Длительность должна быть от 1 до 365 дней") return except ValueError: await message.answer("❌ Введите число") return await state.update_data(days=days) await message.answer( t('enter_sub_traffic', user.language), reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_create_sub_traffic) @admin_router.message(StateFilter(BotStates.admin_create_sub_traffic)) async def handle_sub_traffic(message: Message, state: FSMContext, user: User, api: RemnaWaveAPI = None, **kwargs): """Handle subscription traffic limit input""" try: traffic_gb = int(message.text.strip()) if traffic_gb < 0 or traffic_gb > 10000: await message.answer("❌ Лимит трафика должен быть от 0 до 10000 ГБ") return except ValueError: await message.answer("❌ Введите число") return await state.update_data(traffic_gb=traffic_gb) # Try to get squads from RemnaWave API if api: try: logger.info("Attempting to fetch squads from API") squads = await api.get_internal_squads_list() logger.info(f"API returned squads: {squads}") if squads and len(squads) > 0: logger.info(f"Found {len(squads)} squads, showing selection keyboard") await message.answer( "📋 Выберите Squad из списка:", reply_markup=squad_selection_keyboard(squads, user.language) ) await state.set_state(BotStates.admin_create_sub_squad_select) return else: logger.warning("No squads returned from API or empty list") except Exception as e: logger.error(f"Failed to get squads from API: {e}", exc_info=True) else: logger.warning("No API instance provided") # Fallback to manual input if API fails logger.info("Falling back to manual squad UUID input") await message.answer( t('enter_squad_uuid', user.language), reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_create_sub_squad) def squad_selection_keyboard(squads: List[Dict], language: str = 'ru') -> InlineKeyboardMarkup: """Create keyboard for squad selection""" logger.info(f"Creating squad selection keyboard for {len(squads)} squads") buttons = [] for squad in squads: logger.debug(f"Processing squad: {squad}") # Получаем название и UUID squad'а с проверкой squad_name = squad.get('name', 'Unknown Squad') squad_uuid = squad.get('uuid', '') if not squad_uuid: logger.warning(f"Squad without UUID: {squad}") continue # Truncate name if too long if len(squad_name) > 30: display_name = squad_name[:27] + "..." else: display_name = squad_name # Добавляем информацию о количестве участников если есть info_text = "" if 'info' in squad: members_count = squad['info'].get('membersCount', 0) inbounds_count = squad['info'].get('inboundsCount', 0) info_text = f" ({members_count}👥, {inbounds_count}🔗)" button_text = f"📋 {display_name}{info_text}" logger.debug(f"Creating button: {button_text} -> {squad_uuid}") buttons.append([ InlineKeyboardButton( text=button_text, callback_data=f"select_squad_{squad_uuid}" ) ]) if not buttons: logger.warning("No valid squads found for keyboard") # Добавляем кнопку ручного ввода как единственную опцию buttons.append([ InlineKeyboardButton( text="✏️ Ввести UUID вручную", callback_data="manual_squad_input" ) ]) else: # Add manual input button as alternative buttons.append([ InlineKeyboardButton( text="✏️ Ввести UUID вручную", callback_data="manual_squad_input" ) ]) # Add cancel button buttons.append([ InlineKeyboardButton( text=t('cancel', language), callback_data="main_menu" ) ]) logger.info(f"Created keyboard with {len(buttons)} buttons") return InlineKeyboardMarkup(inline_keyboard=buttons) @admin_router.callback_query(F.data == "manual_squad_input") async def manual_squad_input(callback: CallbackQuery, state: FSMContext, user: User, **kwargs): """Switch to manual squad UUID input""" if not await check_admin_access(callback, user): return await callback.message.edit_text( t('enter_squad_uuid', user.language), reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_create_sub_squad) @admin_router.callback_query(F.data.startswith("select_squad_")) async def handle_squad_selection(callback: CallbackQuery, state: FSMContext, user: User, db: Database, **kwargs): """Handle squad selection from inline keyboard""" if not await check_admin_access(callback, user): return try: squad_uuid = callback.data.replace("select_squad_", "") # Validate UUID format if not validate_squad_uuid(squad_uuid): await callback.answer("❌ Неверный формат UUID") return # Get all state data data = await state.get_data() # Create subscription in database subscription = await db.create_subscription( name=data['name'], description=data['description'], price=data['price'], duration_days=data['days'], traffic_limit_gb=data['traffic_gb'], squad_uuid=squad_uuid ) await callback.message.edit_text( t('subscription_created', user.language), reply_markup=admin_menu_keyboard(user.language) ) log_user_action(user.telegram_id, "subscription_created", data['name']) except Exception as e: logger.error(f"Error creating subscription: {e}") await callback.message.edit_text( t('error_occurred', user.language), reply_markup=admin_menu_keyboard(user.language) ) await state.clear() @admin_router.message(StateFilter(BotStates.admin_create_sub_squad)) async def handle_sub_squad(message: Message, state: FSMContext, user: User, db: Database, **kwargs): """Handle subscription squad UUID manual input (fallback)""" squad_uuid = message.text.strip() if not validate_squad_uuid(squad_uuid): await message.answer("❌ Неверный формат UUID") return # Get all state data data = await state.get_data() try: # Create subscription in database subscription = await db.create_subscription( name=data['name'], description=data['description'], price=data['price'], duration_days=data['days'], traffic_limit_gb=data['traffic_gb'], squad_uuid=squad_uuid ) await message.answer( t('subscription_created', user.language), reply_markup=admin_menu_keyboard(user.language) ) log_user_action(user.telegram_id, "subscription_created", data['name']) except Exception as e: logger.error(f"Error creating subscription: {e}") await message.answer( t('error_occurred', user.language), reply_markup=admin_menu_keyboard(user.language) ) await state.clear() @admin_router.callback_query(F.data == "list_admin_subscriptions") async def list_admin_subscriptions(callback: CallbackQuery, user: User, db: Database, **kwargs): """List all subscriptions for admin""" if not await check_admin_access(callback, user): return try: subs = await db.get_all_subscriptions(include_inactive=True, exclude_trial=True) if not subs: await callback.message.edit_text( "❌ Подписки не найдены", reply_markup=back_keyboard("admin_subscriptions", user.language) ) return keyboard = admin_subscriptions_list_keyboard(subs, user.language) await callback.message.edit_text( t('subscriptions_list', user.language), reply_markup=keyboard ) except Exception as e: logger.error(f"Error listing subscriptions: {e}") await callback.answer(t('error_occurred', user.language)) @admin_router.callback_query(F.data.startswith("toggle_sub_")) async def toggle_subscription(callback: CallbackQuery, user: User, db: Database, **kwargs): """Toggle subscription active status""" if not await check_admin_access(callback, user): return try: sub_id = int(callback.data.split("_")[2]) sub = await db.get_subscription_by_id(sub_id) if not sub: await callback.answer("❌ Подписка не найдена") return sub.is_active = not sub.is_active await db.update_subscription(sub) status = t('enabled', user.language) if sub.is_active else t('disabled', user.language) await callback.answer(f"✅ Подписка «{sub.name}» {status}") # Update the list subs = await db.get_all_subscriptions(include_inactive=True) await callback.message.edit_reply_markup( reply_markup=admin_subscriptions_list_keyboard(subs, user.language) ) except Exception as e: logger.error(f"Error toggling subscription: {e}") await callback.answer(t('error_occurred', user.language)) @admin_router.callback_query(F.data.startswith("edit_sub_")) async def edit_sub_menu(callback: CallbackQuery, state: FSMContext, user: User, **kwargs): """Show subscription edit menu""" if not await check_admin_access(callback, user): return try: sub_id = int(callback.data.split("_")[2]) await state.update_data(edit_sub_id=sub_id) buttons = [ [InlineKeyboardButton(text="📝 Название", callback_data="edit_field_name")], [InlineKeyboardButton(text="💰 Цена", callback_data="edit_field_price")], [InlineKeyboardButton(text="📅 Дни", callback_data="edit_field_days")], [InlineKeyboardButton(text="📊 Трафик", callback_data="edit_field_traffic")], [InlineKeyboardButton(text="📋 Описание", callback_data="edit_field_description")], [InlineKeyboardButton(text=t('back', user.language), callback_data="list_admin_subscriptions")] ] kb = InlineKeyboardMarkup(inline_keyboard=buttons) await callback.message.edit_text("🔧 Выберите поле для редактирования:", reply_markup=kb) except Exception as e: logger.error(f"Error showing edit menu: {e}") await callback.answer(t('error_occurred', user.language)) @admin_router.callback_query(F.data.startswith("edit_field_")) async def ask_new_value(callback: CallbackQuery, state: FSMContext, user: User, **kwargs): """Ask for new field value""" if not await check_admin_access(callback, user): return field = callback.data.split("_")[2] await state.update_data(edit_field=field) field_names = { 'name': 'название', 'price': 'цену', 'days': 'количество дней', 'traffic': 'лимит трафика (ГБ)', 'description': 'описание' } field_name = field_names.get(field, field) await callback.message.edit_text( f"📝 Введите новое значение для поля '{field_name}':", reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_edit_sub_value) @admin_router.message(StateFilter(BotStates.admin_edit_sub_value)) async def handle_edit_value(message: Message, state: FSMContext, user: User, db: Database, **kwargs): """Handle new value for subscription field""" data = await state.get_data() sub_id = data.get('edit_sub_id') field = data.get('edit_field') new_value = message.text.strip() try: sub = await db.get_subscription_by_id(sub_id) if not sub: await message.answer("❌ Подписка не найдена") await state.clear() return # Validate and set new value if field == 'name': if len(new_value) < 3 or len(new_value) > 100: await message.answer("❌ Название должно быть от 3 до 100 символов") return sub.name = new_value elif field == 'price': is_valid, price = is_valid_amount(new_value) if not is_valid: await message.answer(t('invalid_amount', user.language)) return sub.price = price elif field == 'days': try: days = int(new_value) if days <= 0 or days > 365: await message.answer("❌ Длительность должна быть от 1 до 365 дней") return sub.duration_days = days except ValueError: await message.answer("❌ Введите число") return elif field == 'traffic': try: traffic = int(new_value) if traffic < 0 or traffic > 10000: await message.answer("❌ Лимит трафика должен быть от 0 до 10000 ГБ") return sub.traffic_limit_gb = traffic except ValueError: await message.answer("❌ Введите число") return elif field == 'description': if len(new_value) > 500: await message.answer("❌ Описание не должно превышать 500 символов") return sub.description = new_value await db.update_subscription(sub) await message.answer( "✅ Подписка обновлена", reply_markup=admin_menu_keyboard(user.language) ) log_user_action(user.telegram_id, "subscription_edited", f"Sub: {sub.name}, Field: {field}") except Exception as e: logger.error(f"Error updating subscription: {e}") await message.answer(t('error_occurred', user.language)) await state.clear() @admin_router.callback_query(F.data.startswith("delete_sub_")) async def delete_subscription_confirm(callback: CallbackQuery, user: User, **kwargs): """Show subscription deletion confirmation""" if not await check_admin_access(callback, user): return sub_id = int(callback.data.split("_")[2]) keyboard = InlineKeyboardMarkup(inline_keyboard=[ [ InlineKeyboardButton(text="✅ Да, удалить", callback_data=f"confirm_delete_sub_{sub_id}"), InlineKeyboardButton(text="❌ Отмена", callback_data="list_admin_subscriptions") ] ]) await callback.message.edit_text( "⚠️ Вы уверены, что хотите удалить эту подписку?\nЭто действие нельзя отменить!", reply_markup=keyboard ) @admin_router.callback_query(F.data.startswith("confirm_delete_sub_")) async def delete_subscription(callback: CallbackQuery, user: User, db: Database, **kwargs): """Delete subscription""" if not await check_admin_access(callback, user): return try: sub_id = int(callback.data.split("_")[3]) sub = await db.get_subscription_by_id(sub_id) if not sub: await callback.answer("❌ Подписка не найдена") return success = await db.delete_subscription(sub_id) if success: await callback.answer(f"✅ Подписка «{sub.name}» удалена") log_user_action(user.telegram_id, "subscription_deleted", sub.name) else: await callback.answer("❌ Ошибка удаления") # Return to list subs = await db.get_all_subscriptions(include_inactive=True) if subs: await callback.message.edit_text( t('subscriptions_list', user.language), reply_markup=admin_subscriptions_list_keyboard(subs, user.language) ) else: await callback.message.edit_text( "❌ Подписки не найдены", reply_markup=back_keyboard("admin_subscriptions", user.language) ) except Exception as e: logger.error(f"Error deleting subscription: {e}") await callback.answer(t('error_occurred', user.language)) # User management @admin_router.callback_query(F.data == "admin_users") async def admin_users_callback(callback: CallbackQuery, user: User, **kwargs): """Show user management""" if not await check_admin_access(callback, user): return await callback.message.edit_text( t('manage_users', user.language), reply_markup=admin_users_keyboard(user.language) ) @admin_router.callback_query(F.data == "list_users") async def list_users_callback(callback: CallbackQuery, user: User, db: Database, **kwargs): """List all users""" if not await check_admin_access(callback, user): return try: users = await db.get_all_users() if not users: await callback.message.edit_text( "❌ Пользователи не найдены", reply_markup=back_keyboard("admin_users", user.language) ) return text = t('user_list', user.language) + "\n\n" # Show first 20 users for u in users[:20]: username = u.username or "N/A" text += t('user_item', user.language, id=u.telegram_id, username=username, balance=u.balance ) + "\n" if len(users) > 20: text += f"\n... и еще {len(users) - 20} пользователей" await callback.message.edit_text( text, reply_markup=back_keyboard("admin_users", user.language) ) except Exception as e: logger.error(f"Error listing users: {e}") await callback.answer(t('error_occurred', user.language)) # Balance management @admin_router.callback_query(F.data == "admin_balance") async def admin_balance_callback(callback: CallbackQuery, user: User, **kwargs): """Show balance management""" if not await check_admin_access(callback, user): return await callback.message.edit_text( t('manage_balance', user.language), reply_markup=admin_balance_keyboard(user.language) ) @admin_router.callback_query(F.data == "admin_add_balance") async def admin_add_balance_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs): """Start adding balance to user""" if not await check_admin_access(callback, user): return await callback.message.edit_text( t('enter_user_id', user.language), reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_add_balance_user) @admin_router.message(StateFilter(BotStates.admin_add_balance_user)) async def handle_balance_user_id(message: Message, state: FSMContext, user: User, db: Database, **kwargs): """Handle user ID input for balance addition""" telegram_id = parse_telegram_id(message.text) if not telegram_id: await message.answer("❌ Неверный Telegram ID") return # Check if user exists target_user = await db.get_user_by_telegram_id(telegram_id) if not target_user: await message.answer(t('user_not_found', user.language)) return await state.update_data(target_user_id=telegram_id) await message.answer( t('enter_balance_amount', user.language), reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_add_balance_amount) @admin_router.message(StateFilter(BotStates.admin_add_balance_amount)) async def handle_balance_amount(message: Message, state: FSMContext, user: User, db: Database, **kwargs): """Handle balance amount input""" is_valid, amount = is_valid_amount(message.text) if not is_valid: await message.answer(t('invalid_amount', user.language)) return data = await state.get_data() target_user_id = data['target_user_id'] try: # Add balance success = await db.add_balance(target_user_id, amount) if success: # Create payment record await db.create_payment( user_id=target_user_id, amount=amount, payment_type='admin_topup', description=f'Пополнение администратором (ID: {user.telegram_id})', status='completed' ) await message.answer( t('balance_added', user.language), reply_markup=admin_menu_keyboard(user.language) ) log_user_action(user.telegram_id, "admin_balance_added", f"User: {target_user_id}, Amount: {amount}") else: await message.answer(t('user_not_found', user.language)) except Exception as e: logger.error(f"Error adding balance: {e}") await message.answer(t('error_occurred', user.language)) await state.clear() @admin_router.callback_query(F.data == "admin_payment_history") async def admin_payment_history_callback(callback: CallbackQuery, user: User, db: Database, state: FSMContext, **kwargs): """Show payment history (first page) - ADMIN VERSION""" logger.info(f"admin_payment_history_callback called for user {user.telegram_id}") if not await check_admin_access(callback, user): logger.warning(f"Admin access denied for user {user.telegram_id}") return logger.info("Admin access granted, clearing state and showing payment history") await state.clear() # Очищаем состояние для новой пагинации await show_payment_history_page(callback, user, db, state, page=0) async def show_payment_history_page(callback: CallbackQuery, user: User, db: Database, state: FSMContext, page: int = 0): """Show payment history page with pagination""" logger.info(f"show_payment_history_page called: page={page}, user={user.telegram_id}") try: page_size = 10 offset = page * page_size # Получаем все платежи с пагинацией payments, total_count = await db.get_all_payments_paginated(offset=offset, limit=page_size) logger.info(f"Got {len(payments) if payments else 0} payments, total_count={total_count}") if not payments and page == 0: await callback.message.edit_text( "❌ История платежей пуста", reply_markup=back_keyboard("admin_balance", user.language) ) return # Если страница пустая, но не первая - возвращаемся на предыдущую if not payments and page > 0: await show_payment_history_page(callback, user, db, state, page - 1) return # Формируем текст total_pages = (total_count + page_size - 1) // page_size text = f"💳 История платежей (стр. {page + 1}/{total_pages})\n" text += f"📊 Всего записей: {total_count}\n\n" for payment in payments: # Получаем информацию о пользователе payment_user = await db.get_user_by_telegram_id(payment.user_id) username = payment_user.username if payment_user and payment_user.username else "N/A" first_name = payment_user.first_name if payment_user and payment_user.first_name else "N/A" # Форматируем статус status_emoji = { 'completed': '✅', 'pending': '⏳', 'cancelled': '❌' }.get(payment.status, '❓') # Форматируем тип платежа type_emoji = { 'topup': '💰', 'subscription': '📱', 'subscription_extend': '🔄', 'promocode': '🎫', 'trial': '🆓', 'admin_topup': '👨‍💼' }.get(payment.payment_type, '💳') date_str = format_datetime(payment.created_at, user.language) amount_str = f"+{payment.amount}" if payment.amount > 0 else str(payment.amount) text += f"{status_emoji} {type_emoji} {amount_str} руб.\n" text += f"👤 {first_name} (@{username}) ID:{payment.user_id}\n" text += f"📝 {payment.description}\n" text += f"📅 {date_str}\n\n" # Сохраняем текущую страницу в состояние await state.update_data(current_page=page) await state.set_state(BotStates.admin_payment_history_page) # Создаем клавиатуру с пагинацией keyboard = create_pagination_keyboard(page, total_pages, "payment_history", user.language) await callback.message.edit_text( text, reply_markup=keyboard ) except Exception as e: logger.error(f"Error showing payment history: {e}") await callback.message.edit_text( t('error_occurred', user.language), reply_markup=back_keyboard("admin_balance", user.language) ) def create_pagination_keyboard(current_page: int, total_pages: int, callback_prefix: str, language: str) -> InlineKeyboardMarkup: """Create pagination keyboard""" buttons = [] # Кнопки навигации nav_buttons = [] if current_page > 0: nav_buttons.append(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"{callback_prefix}_page_{current_page - 1}")) if current_page < total_pages - 1: nav_buttons.append(InlineKeyboardButton(text="Вперед ➡️", callback_data=f"{callback_prefix}_page_{current_page + 1}")) if nav_buttons: buttons.append(nav_buttons) # Индикатор страницы if total_pages > 1: buttons.append([InlineKeyboardButton(text=f"📄 {current_page + 1}/{total_pages}", callback_data="noop")]) # Кнопка "Назад" buttons.append([InlineKeyboardButton(text=t('back', language), callback_data="admin_balance")]) return InlineKeyboardMarkup(inline_keyboard=buttons) @admin_router.callback_query(F.data.startswith("payment_history_page_")) async def payment_history_page_callback(callback: CallbackQuery, user: User, db: Database, state: FSMContext, **kwargs): """Handle payment history pagination""" if not await check_admin_access(callback, user): return try: page = int(callback.data.split("_")[-1]) await show_payment_history_page(callback, user, db, state, page) except (ValueError, IndexError) as e: logger.error(f"Error parsing page number: {e}") await callback.answer("❌ Ошибка навигации") @admin_router.callback_query(F.data == "noop") async def noop_callback(callback: CallbackQuery, **kwargs): """Handle no-operation callback (for page indicator)""" await callback.answer() # Payment approval handlers @admin_router.callback_query(F.data.startswith("approve_payment_")) async def approve_payment(callback: CallbackQuery, user: User, db: Database, **kwargs): """Approve payment""" if not await check_admin_access(callback, user): return try: payment_id = int(callback.data.split("_")[2]) payment = await db.get_payment_by_id(payment_id) if not payment: await callback.answer("❌ Платеж не найден") return if payment.status != 'pending': await callback.answer("❌ Платеж уже обработан") return # Add balance to user success = await db.add_balance(payment.user_id, payment.amount) if success: # Update payment status payment.status = 'completed' await db.update_payment(payment) await callback.message.edit_text( f"✅ Платеж одобрен!\n💰 Пользователю {payment.user_id} добавлено {payment.amount} руб." ) # Notify user about successful payment bot = kwargs.get('bot') if bot: try: await bot.send_message( payment.user_id, f"✅ Ваш баланс пополнен на {payment.amount} руб." ) except Exception as e: logger.error(f"Failed to notify user {payment.user_id}: {e}") log_user_action(user.telegram_id, "payment_approved", f"Payment: {payment_id}, Amount: {payment.amount}") else: await callback.answer("❌ Ошибка при пополнении баланса") except Exception as e: logger.error(f"Error approving payment: {e}") await callback.answer(t('error_occurred', user.language)) @admin_router.callback_query(F.data.startswith("reject_payment_")) async def reject_payment(callback: CallbackQuery, user: User, db: Database, **kwargs): """Reject payment""" if not await check_admin_access(callback, user): return try: payment_id = int(callback.data.split("_")[2]) payment = await db.get_payment_by_id(payment_id) if not payment: await callback.answer("❌ Платеж не найден") return if payment.status != 'pending': await callback.answer("❌ Платеж уже обработан") return # Update payment status payment.status = 'cancelled' await db.update_payment(payment) await callback.message.edit_text( f"❌ Платеж отклонен!\n💰 Платеж пользователя {payment.user_id} на сумму {payment.amount} руб. отклонен." ) # Notify user about rejected payment bot = kwargs.get('bot') if bot: try: await bot.send_message( payment.user_id, f"❌ Ваш запрос на пополнение баланса на {payment.amount} руб. отклонен." ) except Exception as e: logger.error(f"Failed to notify user {payment.user_id}: {e}") log_user_action(user.telegram_id, "payment_rejected", f"Payment: {payment_id}, Amount: {payment.amount}") except Exception as e: logger.error(f"Error rejecting payment: {e}") await callback.answer(t('error_occurred', user.language)) # Promocode management @admin_router.callback_query(F.data == "admin_promocodes") async def admin_promocodes_callback(callback: CallbackQuery, user: User, **kwargs): """Show promocode management""" if not await check_admin_access(callback, user): return await callback.message.edit_text( t('manage_promocodes', user.language), reply_markup=admin_promocodes_keyboard(user.language) ) @admin_router.callback_query(F.data == "create_promocode") async def create_promocode_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs): """Start promocode creation""" if not await check_admin_access(callback, user): return await callback.message.edit_text( t('enter_promo_code', user.language), reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_create_promo_code) @admin_router.message(StateFilter(BotStates.admin_create_promo_code)) async def handle_promo_code(message: Message, state: FSMContext, user: User, db: Database, **kwargs): """Handle promocode input""" code = message.text.strip().upper() if not validate_promocode_format(code): await message.answer("❌ Промокод должен содержать только буквы и цифры (3-20 символов)") return # Check if promocode already exists existing = await db.get_promocode_by_code(code) if existing: await message.answer(t('promocode_exists', user.language)) return await state.update_data(code=code) await message.answer( t('enter_promo_discount', user.language), reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_create_promo_discount) @admin_router.message(StateFilter(BotStates.admin_create_promo_discount)) async def handle_promo_discount(message: Message, state: FSMContext, user: User, **kwargs): """Handle promocode discount input""" is_valid, discount = is_valid_amount(message.text) if not is_valid: await message.answer(t('invalid_amount', user.language)) return await state.update_data(discount=discount) await message.answer( t('enter_promo_limit', user.language), reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_create_promo_limit) @admin_router.message(StateFilter(BotStates.admin_create_promo_limit)) async def handle_promo_limit(message: Message, state: FSMContext, user: User, db: Database, **kwargs): """Handle promocode usage limit input""" try: limit = int(message.text.strip()) if limit <= 0 or limit > 10000: await message.answer("❌ Лимит должен быть от 1 до 10000") return except ValueError: await message.answer("❌ Введите число") return data = await state.get_data() try: # Create promocode promocode = await db.create_promocode( code=data['code'], discount_amount=data['discount'], usage_limit=limit ) await message.answer( t('promocode_created', user.language), reply_markup=admin_menu_keyboard(user.language) ) log_user_action(user.telegram_id, "promocode_created", data['code']) except Exception as e: logger.error(f"Error creating promocode: {e}") await message.answer( t('error_occurred', user.language), reply_markup=admin_menu_keyboard(user.language) ) await state.clear() @admin_router.callback_query(F.data == "list_promocodes") async def list_promocodes_callback(callback: CallbackQuery, user: User, db: Database, **kwargs): """List all promocodes""" if not await check_admin_access(callback, user): return try: promocodes = await db.get_all_promocodes() if not promocodes: await callback.message.edit_text( "❌ Промокоды не найдены", reply_markup=back_keyboard("admin_promocodes", user.language) ) return text = "📋 Список промокодов:\n\n" for promo in promocodes[:10]: # Show first 10 status = "🟢" if promo.is_active else "🔴" expiry = "" if promo.expires_at: expiry = f" (до {format_date(promo.expires_at, user.language)})" text += f"{status} `{promo.code}` - {promo.discount_amount}р.\n" text += f" Использовано: {promo.used_count}/{promo.usage_limit}{expiry}\n\n" if len(promocodes) > 10: text += f"... и еще {len(promocodes) - 10} промокодов" await callback.message.edit_text( text, reply_markup=back_keyboard("admin_promocodes", user.language), parse_mode='Markdown' ) except Exception as e: logger.error(f"Error listing promocodes: {e}") await callback.answer(t('error_occurred', user.language)) # Cancel handlers for admin states @admin_router.callback_query(F.data == "main_menu", StateFilter( BotStates.admin_create_sub_name, BotStates.admin_create_sub_desc, BotStates.admin_create_sub_price, BotStates.admin_create_sub_days, BotStates.admin_create_sub_traffic, BotStates.admin_create_sub_squad, BotStates.admin_add_balance_user, BotStates.admin_add_balance_amount, BotStates.admin_create_promo_code, BotStates.admin_create_promo_discount, BotStates.admin_create_promo_limit, BotStates.admin_edit_sub_value, BotStates.admin_create_sub_name, BotStates.admin_create_sub_desc, BotStates.admin_create_sub_price, BotStates.admin_create_sub_days, BotStates.admin_create_sub_traffic, BotStates.admin_create_sub_squad, BotStates.admin_add_balance_user, BotStates.admin_add_balance_amount, BotStates.admin_create_promo_code, BotStates.admin_create_promo_discount, BotStates.admin_create_promo_limit, BotStates.admin_edit_sub_value, BotStates.admin_send_message_user, BotStates.admin_send_message_text, BotStates.admin_broadcast_text, BotStates.admin_payment_history_page )) @admin_router.callback_query(F.data == "admin_messages") async def admin_messages_callback(callback: CallbackQuery, user: User, **kwargs): """Show message management""" if not await check_admin_access(callback, user): return await callback.message.edit_text( t('send_message', user.language), reply_markup=admin_messages_keyboard(user.language) ) @admin_router.callback_query(F.data == "admin_send_to_user") async def admin_send_to_user_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs): """Start sending message to specific user""" if not await check_admin_access(callback, user): return await callback.message.edit_text( t('enter_user_id_message', user.language), reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_send_message_user) @admin_router.message(StateFilter(BotStates.admin_send_message_user)) async def handle_message_user_id(message: Message, state: FSMContext, user: User, db: Database, **kwargs): """Handle user ID input for message sending""" telegram_id = parse_telegram_id(message.text) if not telegram_id: await message.answer("❌ Неверный Telegram ID") return # Check if user exists target_user = await db.get_user_by_telegram_id(telegram_id) if not target_user: await message.answer(t('user_not_found', user.language)) return await state.update_data(target_user_id=telegram_id) await message.answer( t('enter_message_text', user.language), reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_send_message_text) # Monitor service management @admin_router.callback_query(F.data == "admin_monitor") async def admin_monitor_callback(callback: CallbackQuery, user: User, **kwargs): """Show monitor service management""" if not await check_admin_access(callback, user): return await callback.message.edit_text( "🔍 Управление сервисом мониторинга", reply_markup=admin_monitor_keyboard(user.language) ) @admin_router.callback_query(F.data == "monitor_status") async def monitor_status_callback(callback: CallbackQuery, user: User, **kwargs): """Show monitor service status""" if not await check_admin_access(callback, user): return monitor_service = kwargs.get('monitor_service') if not monitor_service: await callback.message.edit_text( "❌ Сервис мониторинга недоступен", reply_markup=back_keyboard("admin_monitor", user.language) ) return try: status = await monitor_service.get_service_status() status_text = "🔍 Статус сервиса мониторинга:\n\n" status_text += f"🟢 Работает: {'Да' if status['is_running'] else 'Нет'}\n" status_text += f"⏱ Интервал проверки: {status['check_interval']} сек\n" status_text += f"🕙 Время ежедневной проверки: {status['daily_check_hour']}:00\n" status_text += f"⚠️ Предупреждение за: {status['warning_days']} дней\n" if status['last_check']: status_text += f"🕐 Последняя проверка: {status['last_check']}" await callback.message.edit_text( status_text, reply_markup=back_keyboard("admin_monitor", user.language) ) except Exception as e: logger.error(f"Error getting monitor status: {e}") await callback.message.edit_text( "❌ Ошибка получения статуса", reply_markup=back_keyboard("admin_monitor", user.language) ) @admin_router.callback_query(F.data == "monitor_force_check") async def monitor_force_check_callback(callback: CallbackQuery, user: User, **kwargs): """Force daily check""" if not await check_admin_access(callback, user): return monitor_service = kwargs.get('monitor_service') if not monitor_service: await callback.answer("❌ Сервис мониторинга недоступен") return try: await callback.answer("⏳ Запускаю принудительную проверку...") await monitor_service.force_daily_check() await callback.message.edit_text( "✅ Принудительная проверка завершена", reply_markup=back_keyboard("admin_monitor", user.language) ) except Exception as e: logger.error(f"Error forcing check: {e}") await callback.message.edit_text( "❌ Ошибка при выполнении проверки", reply_markup=back_keyboard("admin_monitor", user.language) ) @admin_router.callback_query(F.data == "monitor_deactivate_expired") async def monitor_deactivate_expired_callback(callback: CallbackQuery, user: User, **kwargs): """Deactivate expired subscriptions""" if not await check_admin_access(callback, user): return monitor_service = kwargs.get('monitor_service') if not monitor_service: await callback.answer("❌ Сервис мониторинга недоступен") return try: await callback.answer("⏳ Деактивирую истекшие подписки...") count = await monitor_service.deactivate_expired_subscriptions() await callback.message.edit_text( f"✅ Деактивировано {count} истекших подписок", reply_markup=back_keyboard("admin_monitor", user.language) ) log_user_action(user.telegram_id, "expired_subscriptions_deactivated", f"Count: {count}") except Exception as e: logger.error(f"Error deactivating expired subscriptions: {e}") await callback.message.edit_text( "❌ Ошибка при деактивации подписок", reply_markup=back_keyboard("admin_monitor", user.language) ) @admin_router.callback_query(F.data == "monitor_test_user") async def monitor_test_user_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs): """Test monitor for specific user""" if not await check_admin_access(callback, user): return await callback.message.edit_text( "👤 Введите Telegram ID пользователя для тестирования уведомлений:", reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_test_monitor_user) @admin_router.message(StateFilter(BotStates.admin_test_monitor_user)) async def handle_monitor_test_user(message: Message, state: FSMContext, user: User, **kwargs): """Handle user ID for monitor testing""" telegram_id = parse_telegram_id(message.text) if not telegram_id: await message.answer("❌ Неверный Telegram ID") return monitor_service = kwargs.get('monitor_service') if not monitor_service: await message.answer("❌ Сервис мониторинга недоступен") await state.clear() return try: results = await monitor_service.check_single_user(telegram_id) if not results: await message.answer("❌ Результаты не получены") else: text = f"📊 Результаты тестирования для пользователя {telegram_id}:\n\n" for result in results: status = "✅" if result.success else "❌" text += f"{status} {result.message}\n" if result.error: text += f" Ошибка: {result.error}\n" await message.answer( text, reply_markup=admin_menu_keyboard(user.language) ) log_user_action(user.telegram_id, "monitor_test_user", f"User: {telegram_id}") except Exception as e: logger.error(f"Error testing monitor for user: {e}") await message.answer("❌ Ошибка при тестировании") await state.clear() @admin_router.message(StateFilter(BotStates.admin_send_message_text)) async def handle_send_message(message: Message, state: FSMContext, user: User, **kwargs): """Handle message text input and send message""" message_text = message.text.strip() if len(message_text) < 1: await message.answer("❌ Сообщение не может быть пустым") return data = await state.get_data() target_user_id = data['target_user_id'] try: bot = kwargs.get('bot') if bot: await bot.send_message(target_user_id, message_text) await message.answer( t('message_sent', user.language), reply_markup=admin_menu_keyboard(user.language) ) log_user_action(user.telegram_id, "message_sent", f"To user: {target_user_id}") else: await message.answer("❌ Ошибка отправки сообщения") except Exception as e: logger.error(f"Error sending message: {e}") await message.answer("❌ Ошибка отправки сообщения (пользователь заблокировал бота?)") await state.clear() @admin_router.callback_query(F.data == "admin_send_to_all") async def admin_send_to_all_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs): """Start broadcast message""" if not await check_admin_access(callback, user): return await callback.message.edit_text( t('enter_message_text', user.language), reply_markup=cancel_keyboard(user.language) ) await state.set_state(BotStates.admin_broadcast_text) @admin_router.callback_query(F.data == "main_menu", StateFilter(BotStates.admin_test_monitor_user)) async def cancel_monitor_test(callback: CallbackQuery, state: FSMContext, user: User, **kwargs): """Cancel monitor test""" await state.clear() await callback.message.edit_text( t('main_menu', user.language), reply_markup=main_menu_keyboard(user.language, user.is_admin) ) @admin_router.message(StateFilter(BotStates.admin_broadcast_text)) async def handle_broadcast_message(message: Message, state: FSMContext, user: User, db: Database, **kwargs): """Handle broadcast message""" message_text = message.text.strip() if len(message_text) < 1: await message.answer("❌ Сообщение не может быть пустым") return try: # Get all users users = await db.get_all_users() if not users: await message.answer("❌ Пользователи не найдены") await state.clear() return bot = kwargs.get('bot') if not bot: await message.answer("❌ Ошибка отправки сообщения") await state.clear() return sent_count = 0 error_count = 0 # Show progress message progress_msg = await message.answer(f"📤 Отправка сообщения {len(users)} пользователям...") # Send to all users for target_user in users: try: await bot.send_message(target_user.telegram_id, message_text) sent_count += 1 except Exception as e: logger.warning(f"Failed to send broadcast to {target_user.telegram_id}: {e}") error_count += 1 # Small delay to avoid rate limits await asyncio.sleep(0.05) # Update progress message with results await progress_msg.edit_text( t('broadcast_sent', user.language) + "\n" + t('broadcast_stats', user.language, sent=sent_count, errors=error_count), reply_markup=admin_menu_keyboard(user.language) ) log_user_action(user.telegram_id, "broadcast_sent", f"Sent: {sent_count}, Errors: {error_count}") except Exception as e: logger.error(f"Error sending broadcast: {e}") await message.answer( t('error_occurred', user.language), reply_markup=admin_menu_keyboard(user.language) ) await state.clear() async def cancel_admin_action(callback: CallbackQuery, state: FSMContext, user: User, **kwargs): """Cancel admin action and return to main menu""" await state.clear() await callback.message.edit_text( t('main_menu', user.language), reply_markup=main_menu_keyboard(user.language, user.is_admin) )