Files
remnawave-bedolaga-telegram…/admin_handlers.py
2025-08-09 07:02:43 +03:00

6380 lines
280 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, timezone
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
from referral_utils import process_referral_rewards
try:
from api_error_handlers import (
APIErrorHandler, safe_get_nodes, safe_get_system_users,
safe_restart_nodes, check_api_health, handle_api_errors
)
except ImportError:
logger.warning("api_error_handlers module not found, using fallback functions")
async def safe_get_nodes(api):
try:
return True, await api.get_all_nodes() or []
except Exception as e:
logger.error(f"Error in safe_get_nodes: {e}")
return False, []
async def safe_get_system_users(api):
try:
return True, await api.get_all_system_users_full() or []
except Exception as e:
logger.error(f"Error in safe_get_system_users: {e}")
return False, []
async def safe_restart_nodes(api, all_nodes=True, node_id=None):
try:
if all_nodes:
result = await api.restart_all_nodes()
else:
result = await api.restart_node(node_id)
return bool(result), "Success" if result else "Failed"
except Exception as e:
logger.error(f"Error in safe_restart_nodes: {e}")
return False, str(e)
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):
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):
name = message.text.strip()
if not (3 <= 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):
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):
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):
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):
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)
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")
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:
logger.info(f"Creating squad selection keyboard for {len(squads)} squads")
buttons = []
for squad in squads:
logger.debug(f"Processing squad: {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
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:
buttons.append([
InlineKeyboardButton(
text="✏️ Ввести UUID вручную",
callback_data="manual_squad_input"
)
])
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):
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):
if not await check_admin_access(callback, user):
return
try:
squad_uuid = callback.data.replace("select_squad_", "")
if not validate_squad_uuid(squad_uuid):
await callback.answer("❌ Неверный формат UUID")
return
data = await state.get_data()
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):
squad_uuid = message.text.strip()
if not validate_squad_uuid(squad_uuid):
await message.answer("❌ Неверный формат UUID")
return
data = await state.get_data()
try:
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):
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):
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):
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):
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):
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
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):
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))
@admin_router.callback_query(F.data == "admin_users")
async def admin_users_callback(callback: CallbackQuery, user: User, **kwargs):
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):
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):
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):
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):
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:
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):
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):
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):
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
success = await db.add_balance(payment.user_id, payment.amount)
if success:
payment.status = 'completed'
await db.update_payment(payment)
bot = kwargs.get('bot')
await process_referral_rewards(payment.user_id, payment.amount, payment.id, db, bot)
await callback.message.edit_text(
f"✅ Платеж одобрен!\n💰 Пользователю {payment.user_id} добавлено {payment.amount} руб."
)
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):
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
payment.status = 'cancelled'
await db.update_payment(payment)
await callback.message.edit_text(
f"❌ Платеж отклонен!\n💰 Платеж пользователя {payment.user_id} на сумму {payment.amount} руб. отклонен."
)
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):
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):
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):
code = message.text.strip().upper()
if not validate_promocode_format(code):
await message.answer("❌ Промокод должен содержать только буквы и цифры (3-20 символов)")
return
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):
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, **kwargs):
try:
limit = int(message.text.strip())
if limit <= 0 or limit > 10000:
await message.answer("❌ Лимит должен быть от 1 до 10000")
return
except ValueError:
await message.answer("❌ Введите число")
return
await state.update_data(limit=limit)
await message.answer(
"⏰ Введите срок действия промокода:\n\n"
"• Дату в формате YYYY-MM-DD (например: 2025-12-31)\n"
"• Количество дней (например: 30)\n"
"• Или напишите 'нет' для бессрочного промокода\n\n"
"📝 Введите значение:",
reply_markup=cancel_keyboard(user.language)
)
await state.set_state(BotStates.admin_create_promo_expiry)
@admin_router.message(StateFilter(BotStates.admin_create_promo_expiry))
async def handle_promo_expiry(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
expiry_input = message.text.strip().lower()
expires_at = None
try:
if expiry_input in ['нет', 'no', 'none', '']:
expires_at = None
else:
try:
days = int(expiry_input)
if days <= 0 or days > 3650: # Максимум 10 лет
await message.answer("❌ Количество дней должно быть от 1 до 3650")
return
expires_at = datetime.utcnow() + timedelta(days=days)
except ValueError:
try:
expires_at = datetime.strptime(expiry_input, "%Y-%m-%d")
if expires_at <= datetime.utcnow():
await message.answer("❌ Дата должна быть в будущем")
return
except ValueError:
await message.answer(
"❌ Неверный формат даты\n\n"
"Используйте:\n"
"• YYYY-MM-DD (например: 2025-12-31)\n"
"• Количество дней (например: 30)\n"
"'нет' для бессрочного"
)
return
data = await state.get_data()
try:
promocode = await db.create_promocode(
code=data['code'],
discount_amount=data['discount'],
usage_limit=data['limit'],
expires_at=expires_at
)
success_text = "✅ Промокод создан успешно!\n\n"
success_text += f"🎫 Код: {data['code']}\n"
success_text += f"💰 Скидка: {data['discount']}\n"
success_text += f"📊 Лимит: {data['limit']} использований\n"
if expires_at:
success_text += f"⏰ Действует до: {format_datetime(expires_at, user.language)}\n"
else:
success_text += f"⏰ Срок: Бессрочный\n"
await message.answer(
success_text,
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)
)
except Exception as e:
logger.error(f"Error parsing promocode expiry: {e}")
await message.answer(
"❌ Ошибка обработки срока действия",
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):
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
regular_promocodes = []
referral_codes = []
current_time = datetime.utcnow()
for promo in promocodes:
if promo.code.startswith('REF'):
referral_codes.append(promo)
else:
regular_promocodes.append(promo)
expired_count = 0
active_count = 0
for promo in regular_promocodes:
if promo.expires_at and promo.expires_at < current_time:
expired_count += 1
elif promo.is_active:
active_count += 1
current_time_str = current_time.strftime("%H:%M:%S")
text = "📋 Управление промокодами\n\n"
text += f"📊 Статистика:\n"
text += f"Всего промокодов: {len(regular_promocodes)}\n"
text += f"• Активных: {active_count}\n"
text += f"• Истекших: {expired_count}\n"
text += f"• Реферальных кодов: {len(referral_codes)}\n\n"
if regular_promocodes:
text += "🎫 Нажмите на промокод для управления\n\n"
else:
text += "🎫 Обычных промокодов нет\n\n"
if referral_codes:
text += f"👥 Реферальных кодов: {len(referral_codes)} (автоматические)\n"
text += f"\n🕐 Обновлено: {current_time_str}"
try:
await callback.message.edit_text(
text,
reply_markup=promocodes_management_keyboard(regular_promocodes, user.language)
)
except Exception as edit_error:
if "message is not modified" in str(edit_error).lower():
await callback.answer("✅ Список обновлен", show_alert=False)
else:
logger.error(f"Error editing promocodes message: {edit_error}")
await callback.answer("❌ Ошибка обновления", show_alert=True)
except Exception as e:
logger.error(f"Error listing promocodes: {e}")
await callback.answer(t('error_occurred', user.language))
def promocodes_management_keyboard(promocodes: List, language: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
for promo in promocodes[:10]:
status_icon = "🟢" if promo.is_active else "🔴"
if promo.expires_at and promo.expires_at < datetime.utcnow():
status_icon = ""
promo_text = f"{status_icon} {promo.code} ({promo.used_count}/{promo.usage_limit})"
buttons.append([
InlineKeyboardButton(
text=promo_text,
callback_data=f"promo_info_{promo.id}"
)
])
if len(promocodes) > 10:
buttons.append([
InlineKeyboardButton(text=f"... и еще {len(promocodes) - 10}", callback_data="noop")
])
buttons.extend([
[
InlineKeyboardButton(text="🎫 Создать промокод", callback_data="create_promocode"),
InlineKeyboardButton(text="📊 Статистика", callback_data="promocodes_stats")
],
[
InlineKeyboardButton(text="🧹 Очистить истекшие", callback_data="cleanup_expired_promos"),
InlineKeyboardButton(text="🔄 Обновить", callback_data="list_promocodes")
],
[InlineKeyboardButton(text=t('back', language), callback_data="admin_promocodes")]
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
@admin_router.callback_query(F.data.startswith("toggle_promo_"))
async def toggle_promocode_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
if not await check_admin_access(callback, user):
return
try:
promo_id = int(callback.data.split("_")[2])
promocode = await db.get_promocode_by_id(promo_id)
if not promocode:
await callback.answer("❌ Промокод не найден")
return
if promocode.code.startswith('REF'):
await callback.answer("❌ Нельзя изменять реферальные коды")
return
promocode.is_active = not promocode.is_active
await db.update_promocode(promocode)
status_text = "активирован" if promocode.is_active else "деактивирован"
await callback.answer(f"✅ Промокод {promocode.code} {status_text}")
log_user_action(user.telegram_id, "promocode_toggled", f"Code: {promocode.code}, Active: {promocode.is_active}")
await list_promocodes_callback(callback, user, db, **kwargs)
except Exception as e:
logger.error(f"Error toggling promocode: {e}")
await callback.answer(t('error_occurred', user.language))
@admin_router.callback_query(F.data.startswith("edit_promo_field_"))
async def edit_promocode_field_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
if not await check_admin_access(callback, user):
return
db = kwargs.get('db')
if not db:
await callback.answer("❌ База данных недоступна", show_alert=True)
return
try:
parts = callback.data.split("_")
logger.info(f"Parsing callback data: {callback.data}, parts: {parts}")
if len(parts) < 5:
await callback.answer("❌ Неверный формат данных", show_alert=True)
return
promo_id = int(parts[3])
field = parts[4]
logger.info(f"Editing promocode {promo_id}, field {field}")
await state.update_data(edit_promo_id=promo_id, edit_promo_field=field)
field_names = {
'discount': 'размер скидки (₽)',
'limit': 'лимит использований',
'expiry': 'дату истечения (YYYY-MM-DD или пусто)'
}
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_promo_value)
except Exception as e:
logger.error(f"Error editing promocode field: {e}")
await callback.answer(t('error_occurred', user.language))
@admin_router.callback_query(F.data.startswith("edit_promo_"))
async def edit_promocode_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
if not await check_admin_access(callback, user):
return
db = kwargs.get('db')
if not db:
await callback.answer("❌ База данных недоступна", show_alert=True)
return
try:
if "edit_promo_field_" in callback.data:
await edit_promocode_field_callback(callback, user, state, **kwargs)
return
promo_id = int(callback.data.split("_")[2])
await state.update_data(edit_promo_id=promo_id)
promocode = await db.get_promocode_by_id(promo_id)
if not promocode:
await callback.answer("❌ Промокод не найден")
return
if promocode.code.startswith('REF'):
await callback.answer("❌ Нельзя редактировать реферальные коды")
return
text = f"✏️ Редактирование промокода\n\n"
text += f"📋 Код: `{promocode.code}`\n"
text += f"💰 Скидка: {promocode.discount_amount}\n"
text += f"📊 Лимит: {promocode.usage_limit}\n"
text += f"🔘 Статус: {'Активен' if promocode.is_active else 'Неактивен'}\n"
text += f"📈 Использовано: {promocode.used_count}\n"
if promocode.expires_at:
text += f"⏰ Истекает: {format_datetime(promocode.expires_at, user.language)}\n"
await callback.message.edit_text(
text,
reply_markup=promocode_edit_keyboard(promo_id, user.language),
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error showing promocode edit: {e}")
await callback.answer(t('error_occurred', user.language))
@admin_router.message(StateFilter(BotStates.admin_edit_promo_value))
async def handle_edit_promocode_value(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
data = await state.get_data()
promo_id = data.get('edit_promo_id')
field = data.get('edit_promo_field')
new_value = message.text.strip()
try:
promocode = await db.get_promocode_by_id(promo_id)
if not promocode:
await message.answer("❌ Промокод не найден")
await state.clear()
return
if promocode.code.startswith('REF'):
await message.answer("❌ Нельзя редактировать реферальные коды")
await state.clear()
return
if field == 'discount':
is_valid, amount = is_valid_amount(new_value)
if not is_valid:
await message.answer(t('invalid_amount', user.language))
return
promocode.discount_amount = amount
elif field == 'limit':
try:
limit = int(new_value)
if limit <= 0:
await message.answer("❌ Лимит должен быть больше 0")
return
promocode.usage_limit = limit
except ValueError:
await message.answer("❌ Введите число")
return
elif field == 'expiry':
if new_value.lower() in ['', 'нет', 'no', 'none']:
promocode.expires_at = None
else:
try:
expire_date = datetime.strptime(new_value, "%Y-%m-%d")
if expire_date < datetime.utcnow():
await message.answer("❌ Дата не может быть в прошлом")
return
promocode.expires_at = expire_date
except ValueError:
await message.answer("❌ Неверный формат даты. Используйте YYYY-MM-DD")
return
await db.update_promocode(promocode)
await message.answer(
"✅ Промокод обновлен",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📋 К списку промокодов", callback_data="list_promocodes")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
)
log_user_action(user.telegram_id, "promocode_edited", f"Code: {promocode.code}, Field: {field}")
except Exception as e:
logger.error(f"Error updating promocode: {e}")
await message.answer(t('error_occurred', user.language))
await state.clear()
@admin_router.callback_query(F.data.startswith("delete_promo_"))
async def delete_promocode_confirm_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
db = kwargs.get('db')
if not db:
await callback.answer("❌ База данных недоступна", show_alert=True)
return
try:
promo_id = int(callback.data.split("_")[2])
promocode = await db.get_promocode_by_id(promo_id)
if not promocode:
await callback.answer("❌ Промокод не найден")
return
if promocode.code.startswith('REF'):
await callback.answer("❌ Нельзя удалять реферальные коды")
return
text = f"⚠️ Удаление промокода\n\n"
text += f"📋 Код: `{promocode.code}`\n"
text += f"💰 Скидка: {promocode.discount_amount}\n"
text += f"📊 Использован: {promocode.used_count}/{promocode.usage_limit} раз\n\n"
text += f"❗️ Это действие нельзя отменить!"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да, удалить", callback_data=f"confirm_delete_promo_{promo_id}"),
InlineKeyboardButton(text="❌ Отмена", callback_data="list_promocodes")
]
])
await callback.message.edit_text(
text,
reply_markup=keyboard,
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error showing delete confirmation: {e}")
await callback.answer(t('error_occurred', user.language))
@admin_router.callback_query(F.data.startswith("confirm_delete_promo_"))
async def confirm_delete_promocode_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
if not await check_admin_access(callback, user):
return
try:
promo_id = int(callback.data.split("_")[3])
promocode = await db.get_promocode_by_id(promo_id)
if not promocode:
await callback.answer("❌ Промокод не найден")
return
if promocode.code.startswith('REF'):
await callback.answer("❌ Нельзя удалять реферальные коды")
return
success = await db.delete_promocode(promo_id)
if success:
await callback.answer(f"✅ Промокод {promocode.code} удален")
log_user_action(user.telegram_id, "promocode_deleted", promocode.code)
else:
await callback.answer("❌ Ошибка удаления промокода")
await list_promocodes_callback(callback, user, db, **kwargs)
except Exception as e:
logger.error(f"Error deleting promocode: {e}")
await callback.answer(t('error_occurred', user.language))
@admin_router.callback_query(F.data.startswith("promo_info_"))
async def promocode_info_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
if not await check_admin_access(callback, user):
return
try:
promo_id = int(callback.data.split("_")[2])
promocode = await db.get_promocode_by_id(promo_id)
if not promocode:
await callback.answer("❌ Промокод не найден")
return
usage_records = await db.get_promocode_usage_by_id(promo_id)
text = f"📋 Детальная информация о промокоде\n\n"
text += f"🎫 Код: `{promocode.code}`\n"
text += f"💰 Скидка: {promocode.discount_amount}\n"
text += f"📊 Лимит: {promocode.usage_limit}\n"
text += f"📈 Использовано: {promocode.used_count}\n"
text += f"🔘 Статус: {'🟢 Активен' if promocode.is_active else '🔴 Неактивен'}\n"
if promocode.expires_at:
try:
current_time = datetime.utcnow()
if promocode.expires_at < current_time:
text += f"⏰ Истек: {format_datetime(promocode.expires_at, user.language)}\n"
else:
text += f"⏰ Истекает: {format_datetime(promocode.expires_at, user.language)}\n"
except Exception as date_error:
logger.error(f"Error formatting expiry date: {date_error}")
text += f"⏰ Срок: Ошибка отображения даты\n"
else:
text += f"⏰ Срок: Бессрочный\n"
text += f"📅 Создан: {format_datetime(promocode.created_at, user.language)}\n"
total_discount = promocode.discount_amount * promocode.used_count
text += f"\n💸 Общая сумма скидок: {total_discount}\n"
if promocode.usage_limit > 0:
usage_percent = (promocode.used_count / promocode.usage_limit) * 100
text += f"📊 Использовано: {usage_percent:.1f}%\n"
if usage_records:
text += f"\n📜 Последние использования:\n"
for i, usage in enumerate(usage_records[:5], 1):
usage_date = format_datetime(usage.used_at, user.language)
text += f"{i}. ID:{usage.user_id} - {usage_date}\n"
if len(usage_records) > 5:
text += f"... и еще {len(usage_records) - 5} использований\n"
else:
text += f"\n📜 Промокод еще не использовался\n"
is_referral = promocode.code.startswith('REF')
await callback.message.edit_text(
text,
reply_markup=promocode_info_keyboard(promo_id, is_referral, user.language),
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error showing promocode info: {e}")
await callback.answer(t('error_occurred', user.language))
@admin_router.callback_query(F.data == "cleanup_expired_promos")
async def cleanup_expired_promos_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
try:
db = kwargs.get('db')
expired_promos = await db.get_expired_promocodes()
if not expired_promos:
await callback.answer("✅ Нет истекших промокодов для удаления", show_alert=True)
return
text = f"🧹 Очистка истекших промокодов\n\n"
text += f"Найдено истекших промокодов: {len(expired_promos)}\n\n"
text += f"Примеры:\n"
for i, promo in enumerate(expired_promos[:5], 1):
expired_days = (datetime.utcnow() - promo.expires_at).days
text += f"{i}. `{promo.code}` (истек {expired_days} дн. назад)\n"
if len(expired_promos) > 5:
text += f"... и еще {len(expired_promos) - 5}\n"
text += f"\n⚠️ Все истекшие промокоды будут удалены без возможности восстановления!"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да, удалить все", callback_data="confirm_cleanup_expired"),
InlineKeyboardButton(text="❌ Отмена", callback_data="list_promocodes")
]
])
await callback.message.edit_text(
text,
reply_markup=keyboard,
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error showing cleanup confirmation: {e}")
await callback.answer(t('error_occurred', user.language))
@admin_router.callback_query(F.data == "confirm_cleanup_expired")
async def confirm_cleanup_expired_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
if not await check_admin_access(callback, user):
return
try:
await callback.answer("🧹 Удаляю истекшие промокоды...")
deleted_count = await db.cleanup_expired_promocodes()
if deleted_count > 0:
text = f"✅ Очистка завершена!\n\n"
text += f"Удалено истекших промокодов: {deleted_count}"
log_user_action(user.telegram_id, "expired_promocodes_cleaned", f"Count: {deleted_count}")
else:
text = f" Истекших промокодов не найдено"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📋 К списку промокодов", callback_data="list_promocodes")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
)
except Exception as e:
logger.error(f"Error cleaning up expired promocodes: {e}")
await callback.message.edit_text(
f"❌ Ошибка при удалении истекших промокодов",
reply_markup=back_keyboard("list_promocodes", user.language)
)
@admin_router.callback_query(F.data == "promocodes_stats")
async def promocodes_stats_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
if not await check_admin_access(callback, user):
return
try:
await callback.answer("📊 Собираю статистику...")
stats = await db.get_promocode_stats()
text = f"📊 Статистика промокодов\n\n"
text += f"📋 Общая информация:\n"
text += f"Всего промокодов: {stats['total_promocodes']}\n"
text += f"• Активных: {stats['active_promocodes']}\n"
text += f"• Истекших: {stats['expired_promocodes']}\n"
text += f"• Неактивных: {stats['total_promocodes'] - stats['active_promocodes'] - stats['expired_promocodes']}\n\n"
text += f"📈 Использование:\n"
text += f"Всего использований: {stats['total_usage']}\n"
text += f"• Общая сумма скидок: {stats['total_discount_amount']:.2f}\n"
if stats['total_promocodes'] > 0:
avg_usage = stats['total_usage'] / stats['total_promocodes']
text += f"• Среднее использований на промокод: {avg_usage:.1f}\n"
if stats['top_promocodes']:
text += f"\n🏆 Топ-5 популярных промокодов:\n"
for i, (code, used_count, discount) in enumerate(stats['top_promocodes'], 1):
if used_count > 0:
total_discount = used_count * discount
text += f"{i}. `{code}` - {used_count} исп. ({total_discount:.0f}₽)\n"
text += f"\n🕐 Обновлено: {format_datetime(datetime.utcnow(), user.language)}"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Обновить", callback_data="promocodes_stats")],
[InlineKeyboardButton(text="🧹 Очистить истекшие", callback_data="cleanup_expired_promos")],
[InlineKeyboardButton(text="📋 К списку", callback_data="list_promocodes")]
])
await callback.message.edit_text(
text,
reply_markup=keyboard,
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error getting promocodes stats: {e}")
await callback.answer(t('error_occurred', user.language))
@admin_router.callback_query(F.data == "confirm_deactivate_all")
async def confirm_deactivate_all_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
if not await check_admin_access(callback, user):
return
try:
await callback.answer("🔴 Деактивирую все промокоды...")
deactivated_count = await db.deactivate_all_regular_promocodes()
if deactivated_count > 0:
text = f"✅ Деактивация завершена!\n\n"
text += f"Деактивировано промокодов: {deactivated_count}\n\n"
text += f" Реферальные коды не затронуты"
log_user_action(user.telegram_id, "all_promocodes_deactivated", f"Count: {deactivated_count}")
else:
text = f" Нет активных промокодов для деактивации"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📋 К списку промокодов", callback_data="list_promocodes")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
)
except Exception as e:
logger.error(f"Error deactivating all promocodes: {e}")
await callback.message.edit_text(
f"❌ Ошибка при деактивации промокодов",
reply_markup=back_keyboard("list_promocodes", user.language)
)
@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_promo_value,
BotStates.admin_edit_sub_value,
BotStates.admin_send_message_user,
BotStates.admin_send_message_text,
BotStates.admin_broadcast_text,
BotStates.admin_payment_history_page,
BotStates.admin_search_user_any,
BotStates.admin_edit_user_expiry,
BotStates.admin_edit_user_traffic,
BotStates.admin_test_monitor_user,
BotStates.admin_rename_plans_confirm
))
async def cancel_admin_action(callback: CallbackQuery, state: FSMContext, user: User, **kwargs):
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.callback_query(F.data == "admin_messages")
async def admin_messages_callback(callback: CallbackQuery, user: User, **kwargs):
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):
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):
telegram_id = parse_telegram_id(message.text)
if not telegram_id:
await message.answer("❌ Неверный Telegram ID")
return
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)
@admin_router.callback_query(F.data == "admin_monitor")
async def admin_monitor_callback(callback: CallbackQuery, user: User, **kwargs):
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):
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):
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):
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):
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):
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 i, result in enumerate(results, 1):
success = result.get('success', False)
message_text = result.get('message', 'No message')
error = result.get('error', None)
status = "" if success else ""
text += f"{i}. {status} {message_text}\n"
if error:
text += f" ⚠️ Ошибка: {error}\n"
text += "\n"
try:
config = kwargs.get('config')
if config:
text += f"⚙️ Настройки мониторинга:\n"
text += f"• Предупреждение за: {config.MONITOR_WARNING_DAYS} дней\n"
text += f"• Интервал проверки: {config.MONITOR_CHECK_INTERVAL} сек\n"
text += f"• Ежедневная проверка: {config.MONITOR_DAILY_CHECK_HOUR}:00\n"
except Exception as config_error:
logger.warning(f"Could not get config info: {config_error}")
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):
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):
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):
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):
message_text = message.text.strip()
if len(message_text) < 1:
await message.answer("❌ Сообщение не может быть пустым")
return
try:
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
progress_msg = await message.answer(f"📤 Отправка сообщения {len(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
await asyncio.sleep(0.05)
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()
# System management handlers
@admin_router.callback_query(F.data == "admin_system")
async def admin_system_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
await callback.message.edit_text(
"🖥 Управление системой RemnaWave",
reply_markup=admin_system_keyboard(user.language)
)
@admin_router.callback_query(F.data == "system_stats")
async def system_stats_callback(callback: CallbackQuery, user: User, db: Database, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
await show_system_stats(callback, user, db, api)
@admin_router.callback_query(F.data == "refresh_system_stats")
async def refresh_system_stats_callback(callback: CallbackQuery, user: User, db: Database, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
await callback.answer("🔄 Обновляю статистику...")
await show_system_stats(callback, user, db, api)
@admin_router.callback_query(F.data == "debug_users_api")
async def debug_users_api_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
if not api:
await callback.answer("❌ API недоступен", show_alert=True)
return
try:
await callback.answer("🔍 Анализирую структуру API...")
debug_info = await api.debug_users_api()
text = "🔬 **Отладка API пользователей**\n\n"
if 'error' in debug_info:
text += f"❌ Ошибка: {debug_info['error']}\n"
else:
text += f"📦 Тип ответа: `{debug_info.get('api_response_type', 'unknown')}`\n"
if debug_info.get('api_response_keys'):
text += f"🔑 Ключи ответа: `{', '.join(debug_info['api_response_keys'][:5])}`\n"
if debug_info.get('has_users'):
text += f"✅ Пользователи найдены\n"
text += f"📍 Расположение: `{debug_info.get('users_location', 'unknown')}`\n"
if debug_info.get('first_user_structure'):
text += f"\n📋 **Структура пользователя:**\n"
for field in debug_info['first_user_structure'][:10]:
text += f" • `{field}`\n"
if len(debug_info['first_user_structure']) > 10:
text += f" _... и еще {len(debug_info['first_user_structure']) - 10} полей_\n"
else:
text += "❌ Пользователи не найдены в ответе\n"
if debug_info.get('total_count') is not None:
text += f"\n📊 Всего пользователей: {debug_info['total_count']}\n"
text += f"📍 Поле счетчика: `{debug_info.get('total_count_field', 'unknown')}`\n"
text += "\n--- **Тест получения пользователей** ---\n"
users = await api.get_all_system_users_full()
if users:
text += f"✅ Успешно получено {len(users)} пользователей\n"
active = len([u for u in users if u.get('status') == 'ACTIVE'])
text += f"• Активных: {active}\n"
text += f"• Неактивных: {len(users) - active}\n"
if users:
text += f"\n**Пример пользователя:**\n"
example_user = users[0]
text += f"• Username: `{example_user.get('username', 'N/A')}`\n"
text += f"• Status: `{example_user.get('status', 'N/A')}`\n"
text += f"• UUID: `{str(example_user.get('uuid', 'N/A'))[:20]}...`\n"
else:
text += "Не удалось получить пользователей\n"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Повторить тест", callback_data="debug_users_api")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="system_users")]
])
await callback.message.edit_text(
text,
reply_markup=keyboard,
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error in debug_users_api: {e}", exc_info=True)
await callback.message.edit_text(
f"❌ Ошибка отладки API\n\n{str(e)[:200]}",
reply_markup=back_keyboard("system_users", user.language)
)
@admin_router.callback_query(F.data == "debug_api_comprehensive")
async def debug_api_comprehensive_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
if not api:
await callback.message.edit_text(
"❌ API недоступен для диагностики",
reply_markup=admin_system_keyboard(user.language)
)
return
await callback.answer("🔍 Запуск полной диагностики API...")
endpoints_to_test = [
('/api/nodes', 'GET', 'Ноды'),
('/api/users?limit=3', 'GET', 'Пользователи'),
('/api/internal-squads', 'GET', 'Сквады'),
]
diagnostic_text = "🔬 Диагностика RemnaWave API\n\n"
for endpoint, method, description in endpoints_to_test:
try:
diagnostic_text += f"🔹 {description} ({endpoint}):\n"
debug_result = await api.debug_api_response(endpoint, method)
if debug_result.get('success'):
diagnostic_text += f" ✅ Статус: {debug_result.get('status')}\n"
if 'response_keys' in debug_result:
keys = debug_result['response_keys']
diagnostic_text += f" 🔑 Ключи: {', '.join(keys[:5])}\n"
if 'data_type' in debug_result:
data_type = debug_result['data_type']
diagnostic_text += f" 📊 Тип данных: {data_type}\n"
if 'data_count' in debug_result:
count = debug_result['data_count']
diagnostic_text += f" 📈 Количество: {count}\n"
if 'nodes' in endpoint and debug_result.get('json'):
await analyze_nodes_response(debug_result['json'], diagnostic_text)
if 'users' in endpoint and debug_result.get('json'):
await analyze_users_response(debug_result['json'], diagnostic_text)
else:
diagnostic_text += f" ❌ Ошибка: {debug_result.get('status', 'N/A')}\n"
if 'error' in debug_result:
diagnostic_text += f" 💥 Детали: {debug_result['error'][:50]}...\n"
diagnostic_text += "\n"
except Exception as e:
diagnostic_text += f" 💥 Исключение: {str(e)[:50]}...\n\n"
diagnostic_text += "💡 Рекомендации:\n"
diagnostic_text += "• Проверьте токен авторизации\n"
diagnostic_text += "• Убедитесь в корректности base_url\n"
diagnostic_text += "• Проверьте доступность RemnaWave сервера\n"
diagnostic_text += "• Просмотрите логи на предмет ошибок\n"
diagnostic_text += f"\n🕐 Диагностика завершена: {format_datetime(datetime.now(), user.language)}"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Повторить диагностику", callback_data="debug_api_comprehensive")],
[InlineKeyboardButton(text="📊 Простая статистика", callback_data="system_stats")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_system")]
])
if len(diagnostic_text) > 4000:
diagnostic_text = diagnostic_text[:3900] + "\n\n... (текст обрезан)"
try:
await callback.message.edit_text(diagnostic_text, reply_markup=keyboard)
except Exception as e:
logger.error(f"Failed to send diagnostic results: {e}")
await callback.answer("❌ Ошибка отправки результатов диагностики", show_alert=True)
async def analyze_nodes_response(json_data, diagnostic_text):
try:
nodes_list = []
if isinstance(json_data, dict):
if 'data' in json_data and isinstance(json_data['data'], list):
nodes_list = json_data['data']
elif 'response' in json_data and isinstance(json_data['response'], list):
nodes_list = json_data['response']
elif isinstance(json_data, list):
nodes_list = json_data
if nodes_list:
diagnostic_text += f" 🖥 Найдено нод: {len(nodes_list)}\n"
status_counts = {}
for node in nodes_list:
status = str(node.get('status', 'unknown')).lower()
status_counts[status] = status_counts.get(status, 0) + 1
diagnostic_text += f" 📊 Статусы: {dict(status_counts)}\n"
for i, node in enumerate(nodes_list[:2]):
name = node.get('name', f'Node-{i+1}')
status = node.get('status', 'unknown')
diagnostic_text += f" 📡 {name}: {status}\n"
except Exception as e:
diagnostic_text += f" ⚠️ Ошибка анализа нод: {str(e)[:30]}...\n"
async def analyze_users_response(json_data, diagnostic_text):
try:
users_list = []
if isinstance(json_data, dict):
if 'data' in json_data and isinstance(json_data['data'], list):
users_list = json_data['data']
elif 'response' in json_data and isinstance(json_data['response'], list):
users_list = json_data['response']
elif isinstance(json_data, list):
users_list = json_data
if users_list:
diagnostic_text += f" 👥 Найдено пользователей: {len(users_list)}\n"
active_count = len([u for u in users_list if str(u.get('status', '')).upper() == 'ACTIVE'])
diagnostic_text += f" ✅ Активных: {active_count}\n"
statuses = [str(u.get('status', 'N/A')).upper() for u in users_list[:3]]
diagnostic_text += f" 📊 Примеры статусов: {', '.join(statuses)}\n"
except Exception as e:
diagnostic_text += f" ⚠️ Ошибка анализа пользователей: {str(e)[:30]}...\n"
@admin_router.callback_query(F.data == "nodes_management")
async def nodes_management_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
await show_nodes_management_improved(callback, user, api)
async def show_nodes_management_improved(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None):
try:
if not api:
await callback.message.edit_text(
"❌ API RemnaWave недоступен\n\n"
"Для управления нодами необходимо подключение к API.",
reply_markup=admin_system_keyboard(user.language)
)
return
await callback.answer("🖥 Загружаю информацию о нодах...")
nodes = await api.get_all_nodes()
if not nodes:
await callback.message.edit_text(
"❌ Ноды не найдены\n\n"
"Возможные причины:\n"
"В системе не настроены ноды\n"
"• Проблемы с подключением к API\n"
"• Недостаточно прав доступа",
reply_markup=admin_system_keyboard(user.language)
)
return
online_nodes = []
offline_nodes = []
disabled_nodes = []
for node in nodes:
status = node.get('status', 'unknown')
if status == 'online':
online_nodes.append(node)
elif status == 'disabled':
disabled_nodes.append(node)
else:
offline_nodes.append(node)
from datetime import datetime
current_time = datetime.now().strftime("%H:%M:%S")
text = "🖥 **Управление нодами**\n\n"
text += "📊 **Общая статистика:**\n"
text += f"Всего нод: {len(nodes)}\n"
text += f"├ 🟢 Онлайн: {len(online_nodes)}\n"
text += f"├ 🔴 Оффлайн: {len(offline_nodes)}\n"
text += f"└ ⚫ Отключено: {len(disabled_nodes)}\n\n"
if len(online_nodes) == len(nodes):
text += "🟢 **Система работает нормально**\n\n"
elif len(online_nodes) >= len(nodes) * 0.7:
text += "🟡 **Система работает с предупреждениями**\n\n"
elif len(online_nodes) > 0:
text += "🟠 **Система работает частично**\n\n"
else:
text += "🔴 **Критическое состояние системы**\n\n"
text += "━━━━━━━━━━━━━━━━━━━━\n\n"
if online_nodes:
text += "🟢 **Активные ноды:**\n"
for i, node in enumerate(online_nodes[:3], 1):
text += format_node_info(node, i)
if len(online_nodes) > 3:
text += f" _... и еще {len(online_nodes) - 3} активных нод_\n"
text += "\n"
if offline_nodes:
text += "🔴 **Оффлайн ноды:**\n"
for i, node in enumerate(offline_nodes[:2], 1):
text += format_node_info(node, i)
if len(offline_nodes) > 2:
text += f" _... и еще {len(offline_nodes) - 2} оффлайн нод_\n"
text += "\n"
if disabled_nodes:
text += "⚫ **Отключенные ноды:**\n"
for i, node in enumerate(disabled_nodes[:2], 1):
text += format_node_info(node, i)
if len(disabled_nodes) > 2:
text += f" _... и еще {len(disabled_nodes) - 2} отключенных нод_\n"
text += f"\n🕐 _Обновлено: {current_time}_"
keyboard = nodes_management_keyboard(nodes, user.language)
try:
await callback.message.edit_text(
text,
reply_markup=keyboard,
parse_mode='Markdown'
)
except Exception as edit_error:
if "message is not modified" in str(edit_error).lower():
await callback.answer("✅ Информация о нодах актуальна", show_alert=False)
else:
logger.error(f"Error editing nodes management message: {edit_error}")
await callback.answer("❌ Ошибка обновления", show_alert=True)
except Exception as e:
logger.error(f"Error in show_nodes_management_improved: {e}", exc_info=True)
await callback.message.edit_text(
f"❌ Ошибка загрузки информации о нодах\n\n"
f"Детали: {str(e)[:100]}",
reply_markup=admin_system_keyboard(user.language)
)
def format_node_info(node: Dict, index: int) -> str:
name = node.get('name', f'Node-{index}')
address = node.get('address', 'N/A')
if len(name) > 25:
name = name[:22] + "..."
if len(address) > 30:
address = address[:27] + "..."
text = f"{index}. **{name}**\n"
if address != 'N/A':
text += f" 📍 {address}\n"
if node.get('countryCode'):
text += f" 🌍 {node['countryCode']}\n"
if node.get('cpuUsage') or node.get('memUsage'):
text += " 💻 "
if node.get('cpuUsage'):
cpu = node['cpuUsage']
cpu_emoji = "🔴" if cpu > 80 else "🟡" if cpu > 50 else "🟢"
text += f"CPU: {cpu_emoji} {cpu:.0f}% "
if node.get('memUsage'):
mem = node['memUsage']
mem_emoji = "🔴" if mem > 80 else "🟡" if mem > 50 else "🟢"
text += f"MEM: {mem_emoji} {mem:.0f}%"
text += "\n"
if node.get('usersCount'):
text += f" 👥 Пользователей: {node['usersCount']}\n"
if node.get('trafficUsedBytes'):
traffic_used = format_bytes(node['trafficUsedBytes'])
text += f" 📊 Трафик: {traffic_used}\n"
return text
@admin_router.callback_query(F.data == "refresh_nodes_stats")
async def refresh_nodes_stats_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
await callback.answer("🔄 Обновляю информацию о нодах...")
await show_nodes_management_improved(callback, user, api)
@admin_router.callback_query(F.data.startswith("refresh_nodes_stats_"))
async def refresh_nodes_stats_with_timestamp_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
await callback.answer("🔄 Обновляю информацию о нодах...")
await show_nodes_management_improved(callback, user, api)
@admin_router.callback_query(F.data == "restart_all_nodes")
async def restart_all_nodes_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
await callback.message.edit_text(
"⚠️ Вы уверены, что хотите перезагрузить ВСЕ ноды?\n\n"
"Это может привести к временной недоступности сервиса для всех пользователей!",
reply_markup=confirm_restart_keyboard(None, user.language)
)
@admin_router.callback_query(F.data == "confirm_restart_all_nodes")
async def confirm_restart_all_nodes_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
try:
if not api:
await callback.message.edit_text(
"❌ API недоступен\n\n"
"Невозможно выполнить перезагрузку без подключения к RemnaWave API.\n"
"Обратитесь к администратору для настройки подключения.",
reply_markup=admin_system_keyboard(user.language)
)
return
await callback.answer("🔄 Отправляю команду перезагрузки всех нод...")
logger.info("Attempting to restart all nodes via API")
result = await api.restart_all_nodes()
logger.debug(f"Restart all nodes result: {result}")
if result:
text = "✅ Команда перезагрузки всех нод отправлена успешно!\n\n"
text += "⏳ Пожалуйста, подождите несколько минут для завершения перезагрузки.\n"
text += "💡 Вы можете проверить статус нод через меню управления нодами."
log_user_action(user.telegram_id, "restart_all_nodes", "Success")
else:
text = "❌ Ошибка при отправке команды перезагрузки\n\n"
text += "Возможные причины:\n"
text += "• Ноды уже перезагружаются\n"
text += "• Проблема с API соединением\n"
text += "• Недостаточно прав для операции\n\n"
text += "🔄 Попробуйте повторить операцию через несколько минут"
await callback.message.edit_text(
text,
reply_markup=admin_system_keyboard(user.language)
)
except Exception as e:
logger.error(f"Error restarting all nodes: {e}", exc_info=True)
await callback.message.edit_text(
f"❌ Критическая ошибка при перезагрузке\n\n"
f"Детали: {str(e)[:100]}{'...' if len(str(e)) > 100 else ''}\n\n"
f"Обратитесь к администратору для решения проблемы.",
reply_markup=admin_system_keyboard(user.language)
)
@admin_router.callback_query(F.data.startswith("node_details_"))
async def node_details_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
try:
node_id = callback.data.replace("node_details_", "")
if not api:
await callback.answer("❌ API недоступен", show_alert=True)
return
nodes = await api.get_all_nodes()
node = None
for n in nodes:
if str(n.get('id')) == node_id or str(n.get('uuid')) == node_id:
node = n
break
if not node:
await callback.answer("❌ Нода не найдена", show_alert=True)
return
text = "🖥 **Детальная информация о ноде**\n\n"
text += f"📛 **Название:** {node.get('name', 'Unknown')}\n"
text += f"🆔 **ID:** `{node.get('id', node.get('uuid', 'N/A'))}`\n"
status = node.get('status', 'unknown')
status_emoji = {
'online': '🟢',
'offline': '🔴',
'disabled': '',
'disconnected': '🔴',
'xray_stopped': '🟡'
}.get(status, '')
text += f"🔘 **Статус:** {status_emoji} {status.upper()}\n\n"
text += "📡 **Подключение:**\n"
text += f"├ Подключена: {'' if node.get('isConnected') else ''}\n"
text += f"├ Включена: {'' if not node.get('isDisabled') else ''}\n"
text += f"├ Нода онлайн: {'' if node.get('isNodeOnline') else ''}\n"
text += f"└ Xray работает: {'' if node.get('isXrayRunning') else ''}\n\n"
text += "🌍 **Местоположение:**\n"
if node.get('countryCode'):
text += f"├ Страна: {node['countryCode']}\n"
if node.get('address'):
text += f"└ Адрес: `{node['address']}`\n"
text += "\n"
text += "💻 **Информация о системе:**\n"
if node.get('cpuModel'):
cpu_model = node['cpuModel']
if len(cpu_model) > 40:
cpu_model = cpu_model[:37] + "..."
text += f"├ CPU: {cpu_model}\n"
if node.get('totalRam'):
text += f"├ RAM: {node['totalRam']}\n"
if node.get('nodeVersion'):
text += f"├ Версия ноды: {node['nodeVersion']}\n"
if node.get('xrayVersion'):
text += f"└ Версия Xray: {node['xrayVersion']}\n"
text += "\n"
if node.get('cpuUsage') or node.get('memUsage'):
text += "📊 **Использование ресурсов:**\n"
if node.get('cpuUsage'):
cpu = node['cpuUsage']
cpu_bar = create_progress_bar(cpu)
text += f"├ CPU: {cpu_bar} {cpu:.1f}%\n"
if node.get('memUsage'):
mem = node['memUsage']
mem_bar = create_progress_bar(mem)
text += f"└ RAM: {mem_bar} {mem:.1f}%\n"
text += "\n"
text += "⏱ **Время работы и трафик:**\n"
if node.get('xrayUptime'):
uptime_seconds = int(node['xrayUptime'])
uptime_hours = uptime_seconds // 3600
uptime_days = uptime_hours // 24
uptime_hours = uptime_hours % 24
if uptime_days > 0:
text += f"├ Время работы Xray: {uptime_days}д {uptime_hours}ч\n"
else:
text += f"├ Время работы Xray: {uptime_hours}ч {(uptime_seconds % 3600) // 60}м\n"
if node.get('trafficUsedBytes'):
traffic_used = format_bytes(node['trafficUsedBytes'])
text += f"├ Использовано трафика: {traffic_used}\n"
if node.get('usersCount') is not None:
text += f"└ Активных пользователей: {node['usersCount']}\n"
text += "\n"
if node.get('viewPosition'):
text += f"📌 **Позиция в списке:** {node['viewPosition']}\n\n"
keyboard = create_node_actions_keyboard(node_id, status, user.language)
await callback.message.edit_text(
text,
reply_markup=keyboard,
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error showing node details: {e}")
await callback.answer("❌ Ошибка загрузки информации", show_alert=True)
async def show_system_stats(callback: CallbackQuery, user: User, db: Database, api: RemnaWaveAPI = None, force_refresh: bool = False):
try:
db_stats = await db.get_stats()
current_time = datetime.now()
text = "📊 Системная статистика\n\n"
text += "💾 База данных бота:\n"
text += f"👥 Пользователей: {db_stats['total_users']}\n"
text += f"📋 Подписок: {db_stats['total_subscriptions_non_trial']}\n"
text += f"💰 Доходы: {db_stats['total_revenue']} руб.\n\n"
if api:
text += "🔗 API RemnaWave: 🟢 Подключен\n\n"
try:
logger.info("=== FETCHING ENHANCED SYSTEM STATS ===")
await callback.answer("📊 Загружаю статистику системы...")
system_stats = await api.get_system_stats()
if system_stats:
text += "🖥 Система RemnaWave:\n"
total_users = system_stats.get('total_users', 0)
active_users = system_stats.get('active_users', 0)
disabled_users = system_stats.get('disabled_users', 0)
limited_users = system_stats.get('limited_users', 0)
expired_users = system_stats.get('expired_users', 0)
text += f"👤 Пользователей в системе: {total_users}\n"
text += f"✅ Активных: {active_users}\n"
online_stats = system_stats.get('online_stats', {})
if online_stats:
online_now = online_stats.get('online_now', 0)
last_day = online_stats.get('last_day', 0)
last_week = online_stats.get('last_week', 0)
never_online = online_stats.get('never_online', 0)
text += f"🟢 Онлайн сейчас: {online_now}\n"
text += f"📅 За сутки: {last_day}\n"
text += f"📅 За неделю: {last_week}\n"
if never_online > 0:
text += f"⚫ Никогда не подключались: {never_online}\n"
if disabled_users > 0 or limited_users > 0 or expired_users > 0:
text += f"❌ Неактивных: {disabled_users + limited_users + expired_users}\n"
if disabled_users > 0:
text += f" • Отключено: {disabled_users}\n"
if limited_users > 0:
text += f" • Ограничено: {limited_users}\n"
if expired_users > 0:
text += f" • Истекло: {expired_users}\n"
nodes_info = system_stats.get('nodes', {})
if nodes_info:
total_nodes = nodes_info.get('total', 0)
online_nodes = nodes_info.get('online', 0)
offline_nodes = nodes_info.get('offline', 0)
text += f"\n📡 Ноды ({total_nodes} шт.):\n"
text += f"🟢 Онлайн: {online_nodes}\n"
if offline_nodes > 0:
text += f"🔴 Оффлайн: {offline_nodes}\n"
if total_nodes > 0:
if online_nodes >= total_nodes:
health_status = "🟢 Отличное"
else:
health_percent = (online_nodes / total_nodes) * 100
if health_percent >= 80:
health_status = "🟡 Хорошее"
elif health_percent >= 50:
health_status = "🟠 Удовлетворительное"
else:
health_status = "🔴 Критическое"
text += f"🏥 Состояние: {health_status}\n"
system_resources = system_stats.get('system_resources', {})
if system_resources:
text += f"\n💻 Системные ресурсы:\n"
cpu_info = system_resources.get('cpu', {})
if cpu_info.get('cores'):
cores = cpu_info.get('cores', 0)
physical_cores = cpu_info.get('physical_cores', 0)
text += f"🔧 CPU: {cores} ядер"
if physical_cores != cores:
text += f" ({physical_cores} физических)"
text += "\n"
memory_info = system_resources.get('memory', {})
if memory_info.get('total_gb'):
total_gb = memory_info.get('total_gb', 0)
active_gb = memory_info.get('active_gb', 0)
available_gb = memory_info.get('available_gb', 0)
usage_percent = memory_info.get('usage_percent', 0)
text += f"💾 RAM: {active_gb:.1f}/{total_gb:.1f} ГБ ({usage_percent:.1f}%)\n"
text += f"📈 Доступно: {available_gb:.1f} ГБ\n"
uptime = system_resources.get('uptime', 0)
if uptime > 0:
uptime_hours = int(uptime // 3600)
uptime_days = uptime_hours // 24
uptime_hours = uptime_hours % 24
if uptime_days > 0:
text += f"⏱ Время работы: {uptime_days}д {uptime_hours}ч\n"
else:
text += f"⏱ Время работы: {uptime_hours}ч\n"
total_traffic = system_stats.get('total_traffic_bytes', '0')
if total_traffic and total_traffic != '0':
try:
traffic_bytes = int(total_traffic)
traffic_formatted = format_bytes(traffic_bytes)
text += f"\n📊 Общий трафик пользователей: {traffic_formatted}\n"
except (ValueError, TypeError):
pass
bandwidth_stats = system_stats.get('bandwidth', {})
if bandwidth_stats:
text += f"\n📈 **Трафик системы:**\n"
if 'bandwidthLastTwoDays' in bandwidth_stats:
daily_data = bandwidth_stats['bandwidthLastTwoDays']
current_day = daily_data.get('current', '0')
previous_day = daily_data.get('previous', '0')
difference = daily_data.get('difference', '0')
if current_day != '0':
text += f"За сегодня: {current_day}\n"
if previous_day != '0':
text += f"За вчера: {previous_day}\n"
if difference.startswith('-'):
diff_emoji = "📉"
diff_text = difference[1:]
elif difference.startswith('+') or not difference.startswith('0'):
diff_emoji = "📈"
diff_text = difference.replace('+', '')
else:
diff_emoji = "➡️"
diff_text = "без изменений"
text += f"• Изменение: {diff_emoji} {diff_text}\n"
if 'bandwidthCalendarMonth' in bandwidth_stats:
current_month = bandwidth_stats['bandwidthCalendarMonth'].get('current', '0')
if current_month != '0':
text += f"За месяц: {current_month}\n"
if 'bandwidthCurrentYear' in bandwidth_stats:
current_year = bandwidth_stats['bandwidthCurrentYear'].get('current', '0')
if current_year != '0':
text += f"За год: {current_year}\n"
logger.info(f"Users stats: Total={total_users}, Active={active_users}, Online={online_stats.get('online_now', 0) if online_stats else 0}")
else:
text += "\n❌ Ошибка получения статистики RemnaWave\n"
except Exception as api_error:
logger.error(f"Failed to get RemnaWave stats: {api_error}", exc_info=True)
text += "\n❌ Ошибка получения статистики RemnaWave\n"
text += f"Детали: {str(api_error)[:60]}...\n"
else:
text += "\n🔗 API RemnaWave: 🔴 Недоступен\n"
text += f"\n🕐 Обновлено: {format_datetime(current_time, user.language)}"
keyboard = system_stats_keyboard(user.language, timestamp=int(current_time.timestamp()) if force_refresh else None)
try:
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode='Markdown')
except Exception as edit_error:
if "message is not modified" in str(edit_error).lower():
await callback.answer("✅ Статистика обновлена", show_alert=False)
else:
logger.error(f"Failed to edit system stats message: {edit_error}")
raise edit_error
except Exception as e:
logger.error(f"Critical error in show_system_stats: {e}", exc_info=True)
try:
await callback.message.edit_text(
f"❌ Критическая ошибка получения статистики\n\n"
f"Детали: {str(e)[:100]}{'...' if len(str(e)) > 100 else ''}\n\n"
f"Обратитесь к администратору для решения проблемы.",
reply_markup=admin_system_keyboard(user.language)
)
except:
await callback.answer("❌ Критическая ошибка системы", show_alert=True)
def create_progress_bar(percent: float, length: int = 10) -> str:
filled = int(percent / 100 * length)
bar = '' * filled + '' * (length - filled)
return f"[{bar}]"
def create_node_actions_keyboard(node_id: str, status: str, language: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
if status == 'disabled':
buttons.append([
InlineKeyboardButton(text="✅ Включить ноду", callback_data=f"enable_node_{node_id}")
])
else:
buttons.append([
InlineKeyboardButton(text="⚫ Отключить ноду", callback_data=f"disable_node_{node_id}")
])
buttons.append([
InlineKeyboardButton(text="🔄 Перезагрузить ноду", callback_data=f"restart_node_{node_id}")
])
buttons.append([
InlineKeyboardButton(text="🔄 Обновить информацию", callback_data=f"refresh_node_{node_id}")
])
buttons.append([
InlineKeyboardButton(text="🔙 Назад к списку нод", callback_data="nodes_management")
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
@admin_router.callback_query(F.data.startswith("enable_node_"))
async def enable_node_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
try:
node_id = callback.data.replace("enable_node_", "")
if not api:
await callback.answer("❌ API недоступен", show_alert=True)
return
await callback.answer("🔄 Включаю ноду...")
result = await api.enable_node(node_id)
if result:
await callback.answer("✅ Нода успешно включена", show_alert=True)
log_user_action(user.telegram_id, "node_enabled", f"Node ID: {node_id}")
await node_details_callback(callback, user, api=api)
else:
await callback.answer("❌ Ошибка включения ноды", show_alert=True)
except Exception as e:
logger.error(f"Error enabling node: {e}")
await callback.answer("❌ Ошибка операции", show_alert=True)
@admin_router.callback_query(F.data.startswith("disable_node_"))
async def disable_node_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
try:
node_id = callback.data.replace("disable_node_", "")
if not api:
await callback.answer("❌ API недоступен", show_alert=True)
return
await callback.answer("🔄 Отключаю ноду...")
result = await api.disable_node(node_id)
if result:
await callback.answer("✅ Нода успешно отключена", show_alert=True)
log_user_action(user.telegram_id, "node_disabled", f"Node ID: {node_id}")
await node_details_callback(callback, user, api=api)
else:
await callback.answer("❌ Ошибка отключения ноды", show_alert=True)
except Exception as e:
logger.error(f"Error disabling node: {e}")
await callback.answer("❌ Ошибка операции", show_alert=True)
@admin_router.callback_query(F.data.startswith("restart_node_"))
async def restart_node_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
node_id = callback.data.replace("restart_node_", "")
await callback.message.edit_text(
f"⚠️ Вы уверены, что хотите перезагрузить ноду ID: {node_id}?",
reply_markup=confirm_restart_keyboard(node_id, user.language)
)
@admin_router.callback_query(F.data.startswith("confirm_restart_node_"))
async def confirm_restart_node_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
try:
node_id = callback.data.replace("confirm_restart_node_", "")
await callback.answer("🔄 Перезагружаю ноду...")
if api:
await callback.message.edit_text(
f"✅ Команда перезагрузки ноды {node_id} отправлена!",
reply_markup=admin_system_keyboard(user.language)
)
log_user_action(user.telegram_id, "restart_node", f"Node ID: {node_id}")
else:
await callback.message.edit_text(
"❌ API недоступен",
reply_markup=admin_system_keyboard(user.language)
)
except Exception as e:
logger.error(f"Error restarting node: {e}")
await callback.message.edit_text(
"❌ Ошибка при перезагрузке ноды",
reply_markup=admin_system_keyboard(user.language)
)
@admin_router.callback_query(F.data.startswith("refresh_node_"))
async def refresh_node_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
try:
node_id = callback.data.replace("refresh_node_", "")
if not api:
await callback.answer("❌ API недоступен", show_alert=True)
return
await callback.answer("🔄 Обновляю информацию о ноде...")
nodes = await api.get_all_nodes()
node = None
for n in nodes:
if str(n.get('id')) == node_id or str(n.get('uuid')) == node_id:
node = n
break
if not node:
await callback.answer("❌ Нода не найдена", show_alert=True)
return
from datetime import datetime
current_time = datetime.now().strftime("%H:%M:%S")
text = "🖥 **Детальная информация о ноде**\n\n"
text += f"📛 **Название:** {node.get('name', 'Unknown')}\n"
text += f"🆔 **ID:** `{node.get('id', node.get('uuid', 'N/A'))}`\n"
status = node.get('status', 'unknown')
status_emoji = {
'online': '🟢',
'offline': '🔴',
'disabled': '',
'disconnected': '🔴',
'xray_stopped': '🟡'
}.get(status, '')
text += f"🔘 **Статус:** {status_emoji} {status.upper()}\n\n"
text += "📡 **Подключение:**\n"
text += f"├ Подключена: {'' if node.get('isConnected') else ''}\n"
text += f"├ Включена: {'' if not node.get('isDisabled') else ''}\n"
text += f"├ Нода онлайн: {'' if node.get('isNodeOnline') else ''}\n"
text += f"└ Xray работает: {'' if node.get('isXrayRunning') else ''}\n\n"
text += "🌍 **Местоположение:**\n"
if node.get('countryCode'):
text += f"├ Страна: {node['countryCode']}\n"
if node.get('address'):
text += f"└ Адрес: `{node['address']}`\n"
text += "\n"
text += "💻 **Информация о системе:**\n"
if node.get('cpuModel'):
cpu_model = node['cpuModel']
if len(cpu_model) > 40:
cpu_model = cpu_model[:37] + "..."
text += f"├ CPU: {cpu_model}\n"
if node.get('totalRam'):
text += f"├ RAM: {node['totalRam']}\n"
if node.get('nodeVersion'):
text += f"├ Версия ноды: {node['nodeVersion']}\n"
if node.get('xrayVersion'):
text += f"└ Версия Xray: {node['xrayVersion']}\n"
text += "\n"
if node.get('cpuUsage') or node.get('memUsage'):
text += "📊 **Использование ресурсов:**\n"
if node.get('cpuUsage'):
cpu = node['cpuUsage']
cpu_bar = create_progress_bar(cpu)
text += f"├ CPU: {cpu_bar} {cpu:.1f}%\n"
if node.get('memUsage'):
mem = node['memUsage']
mem_bar = create_progress_bar(mem)
text += f"└ RAM: {mem_bar} {mem:.1f}%\n"
text += "\n"
text += "⏱ **Время работы и трафик:**\n"
if node.get('xrayUptime'):
uptime_seconds = int(node['xrayUptime'])
uptime_hours = uptime_seconds // 3600
uptime_days = uptime_hours // 24
uptime_hours = uptime_hours % 24
if uptime_days > 0:
text += f"├ Время работы Xray: {uptime_days}д {uptime_hours}ч\n"
else:
text += f"├ Время работы Xray: {uptime_hours}ч {(uptime_seconds % 3600) // 60}м\n"
if node.get('trafficUsedBytes'):
traffic_used = format_bytes(node['trafficUsedBytes'])
text += f"├ Использовано трафика: {traffic_used}\n"
if node.get('usersCount') is not None:
text += f"└ Активных пользователей: {node['usersCount']}\n"
text += "\n"
if node.get('viewPosition'):
text += f"📌 **Позиция в списке:** {node['viewPosition']}\n\n"
text += f"🕐 _Обновлено: {current_time}_"
keyboard = create_node_actions_keyboard(node_id, status, user.language)
try:
await callback.message.edit_text(
text,
reply_markup=keyboard,
parse_mode='Markdown'
)
except Exception as edit_error:
if "message is not modified" in str(edit_error).lower():
await callback.answer("✅ Информация актуальна", show_alert=False)
else:
logger.error(f"Error editing node details message: {edit_error}")
await callback.answer("❌ Ошибка обновления", show_alert=True)
except Exception as e:
logger.error(f"Error refreshing node details: {e}")
await callback.answer("❌ Ошибка обновления", show_alert=True)
@admin_router.callback_query(F.data == "system_users")
async def system_users_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
try:
text = "👥 Управление пользователями системы RemnaWave\n\n"
text += "Выберите действие из меню ниже:"
keyboard = system_users_keyboard(user.language)
await callback.message.edit_text(
text,
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Error in system_users_callback: {e}")
await callback.answer("Меню пользователей системы", show_alert=False)
try:
await callback.message.answer(
"👥 Управление пользователями системы RemnaWave\n\nВыберите действие:",
reply_markup=system_users_keyboard(user.language)
)
except Exception as send_error:
logger.error(f"Failed to send new message: {send_error}")
async def safe_edit_message(callback: CallbackQuery, text: str, reply_markup=None, parse_mode=None, answer_text="✅ Обновлено"):
try:
await callback.message.edit_text(
text,
reply_markup=reply_markup,
parse_mode=parse_mode
)
except Exception as e:
if "message is not modified" in str(e).lower():
await callback.answer(answer_text, show_alert=False)
else:
logger.error(f"Error editing message: {e}")
try:
await callback.answer(answer_text, show_alert=False)
except:
pass
@admin_router.callback_query(F.data == "bulk_operations")
async def bulk_operations_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
await callback.message.edit_text(
"🗂 Массовые операции с пользователями\n\n"
"⚠️ Внимание: эти операции затрагивают всех пользователей системы!",
reply_markup=bulk_operations_keyboard(user.language)
)
@admin_router.callback_query(F.data == "bulk_reset_traffic")
async def bulk_reset_traffic_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да, сбросить", callback_data="confirm_bulk_reset_traffic"),
InlineKeyboardButton(text="❌ Отмена", callback_data="bulk_operations")
]
])
await callback.message.edit_text(
"⚠️ Вы уверены, что хотите сбросить трафик для ВСЕХ пользователей системы?",
reply_markup=keyboard
)
@admin_router.callback_query(F.data == "confirm_bulk_reset_traffic")
async def confirm_bulk_reset_traffic_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
try:
await callback.answer("🔄 Сбрасываю трафик для всех пользователей...")
if api:
await callback.message.edit_text("⏳ Выполняется массовый сброс трафика...")
result = await api.bulk_reset_all_traffic()
if result:
await callback.message.edit_text(
"✅ Трафик сброшен для всех пользователей!",
reply_markup=bulk_operations_keyboard(user.language)
)
log_user_action(user.telegram_id, "bulk_reset_traffic", "All users")
else:
await callback.message.edit_text(
"❌ Ошибка при сбросе трафика (возможно, нет пользователей)",
reply_markup=bulk_operations_keyboard(user.language)
)
else:
await callback.message.edit_text(
"❌ API недоступен",
reply_markup=bulk_operations_keyboard(user.language)
)
except Exception as e:
logger.error(f"Error in bulk traffic reset: {e}")
await callback.message.edit_text(
"❌ Ошибка при сбросе трафика",
reply_markup=bulk_operations_keyboard(user.language)
)
@admin_router.callback_query(F.data == "admin_stats")
async def admin_stats_callback(callback: CallbackQuery, user: User, db: Database, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
try:
db_stats = await db.get_stats()
text = "📊 Краткая статистика\n\n"
text += "💾 База данных бота:\n"
text += f"👥 Пользователей: {db_stats['total_users']}\n"
text += f"📋 Подписок: {db_stats['total_subscriptions_non_trial']}\n"
text += f"💰 Доходы: {db_stats['total_revenue']} руб.\n"
if api:
try:
nodes_stats = await api.get_nodes_statistics()
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🖥 Ноды RemnaWave: {online_nodes}/{len(nodes)} онлайн"
except Exception as e:
logger.warning(f"Failed to get quick RemnaWave stats: {e}")
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🖥 Подробная системная статистика", callback_data="admin_system")],
[InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_stats")],
[InlineKeyboardButton(text="🔙 " + t('back', user.language), callback_data="admin_panel")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
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)
)
@admin_router.callback_query(F.data == "list_all_system_users")
async def list_all_system_users_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, state: FSMContext = None, **kwargs):
if not await check_admin_access(callback, user):
return
if state:
await state.clear()
await state.update_data(users_page=0)
if not api:
await callback.message.edit_text(
"❌ API RemnaWave недоступен\n\n"
"Для просмотра пользователей системы необходимо подключение к API.",
reply_markup=back_keyboard("admin_system", user.language)
)
await callback.answer()
return
await show_system_users_list_paginated(callback, user, api, state, page=0)
async def show_system_users_list_paginated(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None,
state: FSMContext = None, page: int = 0):
try:
if not api:
await callback.message.edit_text(
"❌ API недоступен",
reply_markup=system_users_keyboard(user.language)
)
return
await callback.answer("📋 Загружаю список пользователей...")
all_users = await api.get_all_system_users_full()
if not all_users:
await callback.message.edit_text(
"❌ Пользователи не найдены",
reply_markup=system_users_keyboard(user.language)
)
return
all_users.sort(key=lambda x: (
0 if x.get('status') == 'ACTIVE' else 1,
x.get('createdAt', ''),
), reverse=True)
users_per_page = 8
total_pages = (len(all_users) + users_per_page - 1) // users_per_page
start_idx = page * users_per_page
end_idx = min(start_idx + users_per_page, len(all_users))
page_users = all_users[start_idx:end_idx]
active_count = len([u for u in all_users if u.get('status') == 'ACTIVE'])
disabled_count = len(all_users) - active_count
with_telegram = len([u for u in all_users if u.get('telegramId')])
text = f"👥 Пользователи системы RemnaWave\n"
text += f"📄 Страница {page + 1} из {total_pages}\n\n"
text += f"📊 Статистика:\n"
text += f"Всего: {len(all_users)}\n"
text += f"├ ✅ Активных: {active_count}\n"
text += f"├ ❌ Отключенных: {disabled_count}\n"
text += f"└ 📱 С Telegram: {with_telegram}\n\n"
text += "━━━━━━━━━━━━━━━━━━━━\n\n"
for i, sys_user in enumerate(page_users, start=start_idx + 1):
status = sys_user.get('status', 'UNKNOWN')
if status == 'ACTIVE':
status_icon = "🟢"
elif status == 'DISABLED':
status_icon = "🔴"
elif status == 'LIMITED':
status_icon = "🟡"
elif status == 'EXPIRED':
status_icon = ""
else:
status_icon = ""
username = sys_user.get('username', 'N/A')
username = username.replace('*', '').replace('_', '').replace('[', '').replace(']', '').replace('`', '')
short_uuid = sys_user.get('shortUuid', '')[:8] + "..." if sys_user.get('shortUuid') else 'N/A'
text += f"{i}. {status_icon} {username}\n"
if sys_user.get('telegramId'):
telegram_id = str(sys_user['telegramId'])
text += f" 📱 TG: {telegram_id}\n"
text += f" 🔗 {short_uuid}\n"
if sys_user.get('expireAt'):
try:
expire_dt = datetime.fromisoformat(sys_user['expireAt'].replace('Z', '+00:00'))
days_left = (expire_dt - datetime.now()).days
if days_left < 0:
text += f" ❌ Истекла {abs(days_left)} дн. назад\n"
elif days_left == 0:
text += f" ⚠️ Истекает сегодня\n"
elif days_left <= 3:
text += f" ⚠️ Осталось {days_left} дн.\n"
else:
text += f" ⏰ До {expire_dt.strftime('%d.%m.%Y')}\n"
except:
expire_date = sys_user['expireAt'][:10] if sys_user['expireAt'] else 'N/A'
text += f"{expire_date}\n"
traffic_limit = sys_user.get('trafficLimitBytes', 0)
used_traffic = sys_user.get('usedTrafficBytes', 0)
if traffic_limit > 0:
usage_percent = (used_traffic / traffic_limit) * 100
if usage_percent >= 90:
traffic_icon = "🔴"
elif usage_percent >= 70:
traffic_icon = "🟡"
else:
traffic_icon = "🟢"
used_str = format_bytes(used_traffic)
limit_str = format_bytes(traffic_limit)
text += f" 📊 {traffic_icon} {usage_percent:.0f}% ({used_str}/{limit_str})\n"
else:
used_str = format_bytes(used_traffic)
text += f" 📊 ♾️ Безлимит ({used_str})\n"
text += "\n"
keyboard = create_users_pagination_keyboard(page, total_pages, user.language)
await callback.message.edit_text(
text,
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Error showing system users: {e}", exc_info=True)
try:
await callback.message.edit_text(
f"❌ Ошибка загрузки пользователей\n\nДетали: {str(e)[:100]}",
reply_markup=system_users_keyboard(user.language)
)
except:
await callback.answer("❌ Ошибка загрузки пользователей", show_alert=True)
def create_users_pagination_keyboard(current_page: int, total_pages: int, language: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
buttons.append([
InlineKeyboardButton(text="🔍 Поиск", callback_data="search_user_uuid"),
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_users_page_{current_page}")
])
if total_pages > 1:
nav_row = []
if current_page > 0:
nav_row.append(InlineKeyboardButton(text="", callback_data="users_page_0"))
if current_page > 0:
nav_row.append(InlineKeyboardButton(text="◀️", callback_data=f"users_page_{current_page - 1}"))
nav_row.append(InlineKeyboardButton(text=f"{current_page + 1}/{total_pages}", callback_data="noop"))
if current_page < total_pages - 1:
nav_row.append(InlineKeyboardButton(text="▶️", callback_data=f"users_page_{current_page + 1}"))
if current_page < total_pages - 1:
nav_row.append(InlineKeyboardButton(text="", callback_data=f"users_page_{total_pages - 1}"))
buttons.append(nav_row)
buttons.append([
InlineKeyboardButton(text="✅ Только активные", callback_data="filter_users_active"),
InlineKeyboardButton(text="📱 С Telegram", callback_data="filter_users_telegram")
])
buttons.append([
InlineKeyboardButton(text="🔙 Назад", callback_data="system_users")
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
@admin_router.callback_query(F.data.startswith("users_page_"))
async def users_page_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, state: FSMContext = None, **kwargs):
if not await check_admin_access(callback, user):
return
try:
page = int(callback.data.split("_")[-1])
await show_system_users_list_paginated(callback, user, api, state, page)
except Exception as e:
logger.error(f"Error in pagination: {e}")
await callback.answer("❌ Ошибка навигации", show_alert=True)
@admin_router.callback_query(F.data.startswith("refresh_system_users_"))
async def refresh_system_users_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
await show_system_users_list(callback, user, api, force_refresh=True)
def system_stats_keyboard(language: str, timestamp: int = None) -> InlineKeyboardMarkup:
refresh_callback = f"refresh_system_stats_{timestamp}" if timestamp else "refresh_system_stats"
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🖥 Управление нодами", callback_data="nodes_management")],
[InlineKeyboardButton(text="👥 Пользователи системы", callback_data="system_users")],
[InlineKeyboardButton(text="🗂 Массовые операции", callback_data="bulk_operations")],
[InlineKeyboardButton(text="🔄 Обновить", callback_data=refresh_callback)],
[InlineKeyboardButton(text="🔙 " + t('back', language), callback_data="admin_system")]
])
def nodes_management_keyboard(nodes: List[Dict], language: str, timestamp: int = None) -> InlineKeyboardMarkup:
buttons = []
if nodes:
for i, node in enumerate(nodes[:3]):
node_id = node.get('id', f'{i}')
node_name = node.get('name', f'Node-{i+1}')
is_online = (node.get('isConnected', False) and
not node.get('isDisabled', True) and
node.get('isNodeOnline', False) and
node.get('isXrayRunning', False))
status_emoji = "🟢" if is_online else "🔴"
buttons.append([
InlineKeyboardButton(
text=f"{status_emoji} {node_name}",
callback_data=f"node_details_{node_id}"
)
])
buttons.append([
InlineKeyboardButton(text="🔄 Перезагрузить все ноды", callback_data="restart_all_nodes")
])
refresh_callback = f"refresh_nodes_stats_{timestamp}" if timestamp else "refresh_nodes_stats"
buttons.append([
InlineKeyboardButton(text="🔄 Обновить", callback_data=refresh_callback)
])
buttons.append([
InlineKeyboardButton(text="🔙 Назад", callback_data="admin_system")
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
@admin_router.callback_query(F.data.startswith("refresh_system_stats_"))
async def refresh_system_stats_with_timestamp_callback(callback: CallbackQuery, user: User, db: Database, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
await show_system_stats(callback, user, db, api, force_refresh=True)
@admin_router.callback_query(F.data == "users_statistics")
async def users_statistics_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
try:
if not api:
await callback.message.edit_text(
"❌ API недоступен",
reply_markup=system_users_keyboard(user.language)
)
return
await callback.answer("📊 Собираю статистику...")
system_stats = await api.get_system_stats()
users_count = await api.get_users_count()
text = "📊 Детальная статистика пользователей\n\n"
if users_count is not None:
text += f"👥 Всего пользователей: {users_count}\n"
if system_stats:
if 'users' in system_stats:
text += f"• Активных пользователей: {system_stats['users']}\n"
if 'bandwidth' in system_stats:
bandwidth = system_stats['bandwidth']
if bandwidth.get('downlink') or bandwidth.get('uplink'):
text += f"\n📈 Трафик:\n"
text += f"• Загружено: {format_bytes(bandwidth.get('downlink', 0))}\n"
text += f"• Отдано: {format_bytes(bandwidth.get('uplink', 0))}\n"
health_info = await api.get_system_health()
if health_info:
text += f"\n🏥 Состояние системы: {health_info.get('status', 'unknown')}\n"
if 'nodes_online' in health_info and 'nodes_total' in health_info:
text += f"🖥 Ноды: {health_info['nodes_online']}/{health_info['nodes_total']} онлайн\n"
text += f"\n🕐 Обновлено: {format_datetime(datetime.now(), user.language)}"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Обновить", callback_data="users_statistics")],
[InlineKeyboardButton(text="👥 Список пользователей", callback_data="list_all_system_users")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="system_users")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
except Exception as e:
logger.error(f"Error getting users statistics: {e}")
await callback.message.edit_text(
"❌ Ошибка получения статистики",
reply_markup=system_users_keyboard(user.language)
)
@admin_router.callback_query(F.data == "search_user_uuid")
async def search_user_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
if not await check_admin_access(callback, user):
return
await callback.message.edit_text(
"🔍 Поиск пользователя\n\n"
"Вы можете искать по:\n"
"• UUID (полный)\n"
"• Short UUID\n"
"• Telegram ID\n"
"• Username\n"
"• Email\n\n"
"📝 Введите любой идентификатор:",
reply_markup=cancel_keyboard(user.language)
)
await state.set_state(BotStates.admin_search_user_any)
@admin_router.message(StateFilter(BotStates.admin_search_user_any))
async def handle_search_user_any(message: Message, state: FSMContext, user: User, api: RemnaWaveAPI = None, db: Database = None, **kwargs):
search_input = message.text.strip()
if not api:
await message.answer(
"❌ API недоступен",
reply_markup=system_users_keyboard(user.language)
)
await state.clear()
return
try:
search_msg = await message.answer("🔍 Поиск пользователя...")
user_data = None
search_method = None
if validate_squad_uuid(search_input):
user_data = await api.get_user_by_uuid(search_input)
search_method = "UUID"
if not user_data:
try:
telegram_id = int(search_input)
user_data = await api.get_user_by_telegram_id(telegram_id)
search_method = "Telegram ID"
except ValueError:
pass
if not user_data:
user_data = await api.get_user_by_short_uuid(search_input)
search_method = "Short UUID"
if not user_data:
user_data = await api.get_user_by_username(search_input)
search_method = "Username"
if not user_data and '@' in search_input:
user_data = await api.get_user_by_email(search_input)
search_method = "Email"
if not user_data:
await search_msg.edit_text(
f"❌ Пользователь не найден\n\n"
f"Искомое значение: `{search_input}`\n\n"
f"Проверены методы поиска:\n"
f"• UUID\n"
f"• Short UUID\n"
f"• Telegram ID\n"
f"• Username\n"
f"• Email\n\n"
f"Проверьте правильность ввода и попробуйте снова",
reply_markup=system_users_keyboard(user.language),
parse_mode='Markdown'
)
await state.clear()
return
local_user = None
if user_data.get('telegramId') and db:
local_user = await db.get_user_by_telegram_id(user_data['telegramId'])
text = f"👤 Информация о пользователе\n"
text += f"🔍 Найден по: {search_method}\n\n"
text += f"📛 Username: `{user_data.get('username', 'N/A')}`\n"
text += f"🆔 UUID: `{user_data.get('uuid', 'N/A')}`\n"
text += f"🔗 Short UUID: `{user_data.get('shortUuid', 'N/A')}`\n"
if user_data.get('telegramId'):
text += f"📱 Telegram ID: `{user_data.get('telegramId')}`\n"
if local_user:
text += f"💰 Баланс в боте: {local_user.balance} руб.\n"
if user_data.get('email'):
text += f"📧 Email: {user_data.get('email')}\n"
status = user_data.get('status', 'UNKNOWN')
status_emoji = "" if status == 'ACTIVE' else ""
text += f"\n🔘 Статус: {status_emoji} {status}\n"
if user_data.get('expireAt'):
expire_date = user_data['expireAt']
text += f"⏰ Истекает: {expire_date[:10]}\n"
try:
expire_dt = datetime.fromisoformat(expire_date.replace('Z', '+00:00'))
days_left = (expire_dt - datetime.now()).days
if days_left > 0:
text += f"📅 Осталось дней: {days_left}\n"
else:
text += f"❌ Подписка истекла\n"
except:
pass
traffic_limit = user_data.get('trafficLimitBytes', 0)
used_traffic = user_data.get('usedTrafficBytes', 0)
if traffic_limit > 0:
text += f"\n📊 Лимит трафика: {format_bytes(traffic_limit)}\n"
text += f"📈 Использовано: {format_bytes(used_traffic)}\n"
usage_percent = (used_traffic / traffic_limit) * 100
text += f"📉 Использовано: {usage_percent:.1f}%\n"
else:
text += f"\n📊 Лимит трафика: Безлимитный\n"
text += f"📈 Использовано: {format_bytes(used_traffic)}\n"
keyboard = create_user_management_keyboard(user_data.get('uuid'), user_data.get('status'), user.language)
await search_msg.edit_text(text, reply_markup=keyboard)
except Exception as e:
logger.error(f"Error searching user: {e}")
def create_user_management_keyboard(user_uuid: str, status: str, language: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
if status == 'ACTIVE':
buttons.append([
InlineKeyboardButton(text="❌ Отключить", callback_data=f"disable_user_{user_uuid}"),
InlineKeyboardButton(text="🔄 Сбросить трафик", callback_data=f"reset_user_traffic_{user_uuid}")
])
else:
buttons.append([
InlineKeyboardButton(text="✅ Включить", callback_data=f"enable_user_{user_uuid}"),
InlineKeyboardButton(text="🔄 Сбросить трафик", callback_data=f"reset_user_traffic_{user_uuid}")
])
buttons.append([
InlineKeyboardButton(text="📅 Изменить срок", callback_data=f"edit_user_expiry_{user_uuid}"),
InlineKeyboardButton(text="📊 Изменить трафик", callback_data=f"edit_user_traffic_{user_uuid}")
])
buttons.append([
InlineKeyboardButton(text="📈 Статистика", callback_data=f"user_usage_stats_{user_uuid}"),
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_user_{user_uuid}")
])
buttons.append([
InlineKeyboardButton(text="🔍 Новый поиск", callback_data="search_user_uuid"),
InlineKeyboardButton(text="🔙 Назад", callback_data="system_users")
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
@admin_router.callback_query(F.data.startswith("edit_user_expiry_"))
async def edit_user_expiry_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
if not await check_admin_access(callback, user):
return
user_uuid = callback.data.replace("edit_user_expiry_", "")
await state.update_data(edit_user_uuid=user_uuid)
await callback.message.edit_text(
"📅 Изменение срока действия подписки\n\n"
"Введите новую дату истечения:\n"
"• YYYY-MM-DD (например: 2025-12-31)\n"
"• Или количество дней (например: 30)\n\n"
"📝 Введите значение:",
reply_markup=cancel_keyboard(user.language)
)
await state.set_state(BotStates.admin_edit_user_expiry)
@admin_router.message(StateFilter(BotStates.admin_edit_user_expiry))
async def handle_edit_user_expiry(message: Message, state: FSMContext, user: User, api: RemnaWaveAPI = None, **kwargs):
if not api:
await message.answer("❌ API недоступен")
await state.clear()
return
data = await state.get_data()
user_uuid = data.get('edit_user_uuid')
input_value = message.text.strip()
try:
new_expiry = None
try:
days = int(input_value)
if days > 0:
new_expiry = datetime.now() + timedelta(days=days)
except ValueError:
try:
new_expiry = datetime.strptime(input_value, "%Y-%m-%d")
except ValueError:
await message.answer("❌ Неверный формат даты. Используйте YYYY-MM-DD или количество дней")
return
if not new_expiry:
await message.answer("Не удалось определить дату")
return
expiry_str = new_expiry.replace(tzinfo=timezone.utc).isoformat().replace('+00:00', 'Z')
result = await api.update_user(user_uuid, {'expireAt': expiry_str, 'status': 'ACTIVE'})
if result:
await message.answer(
f"✅ Срок действия обновлен!\n\n"
f"Новая дата истечения: {new_expiry.strftime('%Y-%m-%d %H:%M')}",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="👤 Вернуться к пользователю", callback_data=f"refresh_user_{user_uuid}")],
[InlineKeyboardButton(text="🔙 В меню", callback_data="system_users")]
])
)
log_user_action(user.telegram_id, "user_expiry_updated", f"UUID: {user_uuid}, New expiry: {expiry_str}")
else:
await message.answer("❌ Ошибка обновления срока действия")
except Exception as e:
logger.error(f"Error updating user expiry: {e}")
await message.answer(f"❌ Ошибка: {str(e)}")
await state.clear()
@admin_router.message(StateFilter(BotStates.admin_edit_user_expiry))
async def handle_edit_user_expiry(message: Message, state: FSMContext, user: User, api: RemnaWaveAPI = None, **kwargs):
if not api:
await message.answer("❌ API недоступен")
await state.clear()
return
data = await state.get_data()
user_uuid = data.get('edit_user_uuid')
input_value = message.text.strip()
try:
new_expiry = None
try:
days = int(input_value)
if days > 0:
new_expiry = datetime.now() + timedelta(days=days)
except ValueError:
try:
new_expiry = datetime.strptime(input_value, "%Y-%m-%d")
except ValueError:
await message.answer("❌ Неверный формат даты. Используйте YYYY-MM-DD или количество дней")
return
if not new_expiry:
await message.answer("Не удалось определить дату")
return
expiry_str = new_expiry.isoformat() + 'Z'
result = await api.update_user(user_uuid, {'expireAt': expiry_str, 'status': 'ACTIVE'})
if result:
await message.answer(
f"✅ Срок действия обновлен!\n\n"
f"Новая дата истечения: {new_expiry.strftime('%Y-%m-%d %H:%M')}",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="👤 Вернуться к пользователю", callback_data=f"refresh_user_{user_uuid}")],
[InlineKeyboardButton(text="🔙 В меню", callback_data="system_users")]
])
)
log_user_action(user.telegram_id, "user_expiry_updated", f"UUID: {user_uuid}, New expiry: {expiry_str}")
else:
await message.answer("❌ Ошибка обновления срока действия")
except Exception as e:
logger.error(f"Error updating user expiry: {e}")
await message.answer(f"❌ Ошибка: {str(e)}")
await state.clear()
@admin_router.callback_query(F.data.startswith("edit_user_traffic_"))
async def edit_user_traffic_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
if not await check_admin_access(callback, user):
return
user_uuid = callback.data.replace("edit_user_traffic_", "")
await state.update_data(edit_user_uuid=user_uuid)
await callback.message.edit_text(
"📊 Изменение лимита трафика\n\n"
"Введите новый лимит трафика:\n"
"• Число в ГБ (например: 100)\n"
"• 0 для безлимитного трафика\n\n"
"📝 Введите значение:",
reply_markup=cancel_keyboard(user.language)
)
await state.set_state(BotStates.admin_edit_user_traffic)
@admin_router.message(StateFilter(BotStates.admin_edit_user_traffic))
async def handle_edit_user_traffic(message: Message, state: FSMContext, user: User, api: RemnaWaveAPI = None, **kwargs):
if not api:
await message.answer("❌ API недоступен")
await state.clear()
return
data = await state.get_data()
user_uuid = data.get('edit_user_uuid')
try:
traffic_gb = int(message.text.strip())
if traffic_gb < 0:
await message.answer("❌ Значение не может быть отрицательным")
return
result = await api.update_user_traffic_limit(user_uuid, traffic_gb)
if result:
traffic_text = f"{traffic_gb} ГБ" if traffic_gb > 0 else "Безлимитный"
await message.answer(
f"✅ Лимит трафика обновлен!\n\n"
f"Новый лимит: {traffic_text}",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="👤 Вернуться к пользователю", callback_data=f"refresh_user_{user_uuid}")],
[InlineKeyboardButton(text="🔙 В меню", callback_data="system_users")]
])
)
log_user_action(user.telegram_id, "user_traffic_updated", f"UUID: {user_uuid}, New limit: {traffic_gb} GB")
else:
await message.answer("❌ Ошибка обновления лимита трафика")
except ValueError:
await message.answer("❌ Введите число")
except Exception as e:
logger.error(f"Error updating user traffic: {e}")
await message.answer(f"❌ Ошибка: {str(e)}")
await state.clear()
@admin_router.callback_query(F.data.startswith("refresh_user_"))
async def refresh_user_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
user_uuid = callback.data.replace("refresh_user_", "")
if not api:
await callback.answer("❌ API недоступен", show_alert=True)
return
try:
await callback.answer("🔄 Обновляю информацию...")
user_data = await api.get_user_by_uuid(user_uuid)
if not user_data:
await callback.answer("❌ Пользователь не найден", show_alert=True)
return
text = f"👤 Информация о пользователе (обновлено)\n\n"
keyboard = create_user_management_keyboard(user_uuid, user_data.get('status'), user.language)
await callback.message.edit_text(text, reply_markup=keyboard)
except Exception as e:
logger.error(f"Error refreshing user: {e}")
await callback.answer("❌ Ошибка обновления", show_alert=True)
@admin_router.callback_query(F.data == "sync_remnawave")
async def sync_remnawave_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
await callback.message.edit_text(
"🔄 Синхронизация с RemnaWave\n\n"
"Выберите тип синхронизации:",
reply_markup=sync_remnawave_keyboard(user.language)
)
def sync_remnawave_keyboard(language: str = 'ru') -> InlineKeyboardMarkup:
buttons = [
#[InlineKeyboardButton(text="👥 Синхронизировать пользователей", callback_data="sync_users_remnawave")],
#[InlineKeyboardButton(text="📋 Синхронизировать подписки", callback_data="sync_subscriptions_remnawave")],
[InlineKeyboardButton(text="🔄 Полная синхронизация", callback_data="sync_full_remnawave")],
[InlineKeyboardButton(text="👤 Синхронизировать одного", callback_data="sync_single_user")],
[InlineKeyboardButton(text="🌍 ИМПОРТ ВСЕХ по Telegram ID", callback_data="import_all_by_telegram")],
[InlineKeyboardButton(text="📋 Просмотр планов", callback_data="view_imported_plans")],
[InlineKeyboardButton(text="📊 Статус синхронизации", callback_data="sync_status_remnawave")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_system")]
]
return InlineKeyboardMarkup(inline_keyboard=buttons)
@admin_router.callback_query(F.data == "sync_users_remnawave")
async def sync_users_remnawave_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, db: Database = None, **kwargs):
if not await check_admin_access(callback, user):
return
if not api or not db:
await callback.answer("❌ API или база данных недоступны", show_alert=True)
return
try:
await callback.answer("🔄 Запускаю синхронизацию пользователей...")
progress_msg = await callback.message.edit_text("⏳ Синхронизация пользователей...\n\n0% выполнено")
remna_users = await api.get_all_system_users_full()
if not remna_users:
await progress_msg.edit_text(
"Не удалось получить пользователей из RemnaWave",
reply_markup=back_keyboard("sync_remnawave", user.language)
)
return
total_users = len(remna_users)
synced = 0
created = 0
updated = 0
errors = 0
for i, remna_user in enumerate(remna_users):
try:
if i % 10 == 0:
progress = (i / total_users) * 100
await progress_msg.edit_text(
f"⏳ Синхронизация пользователей...\n\n"
f"{progress:.1f}% выполнено\n"
f"Обработано: {i}/{total_users}"
)
telegram_id = remna_user.get('telegramId')
if not telegram_id:
continue
bot_user = await db.get_user_by_telegram_id(telegram_id)
if not bot_user:
bot_user = await db.create_user(
telegram_id=telegram_id,
username=remna_user.get('username'),
language='ru',
is_admin=telegram_id in (kwargs.get('config', {}).ADMIN_IDS if 'config' in kwargs else [])
)
created += 1
if not bot_user.remnawave_uuid:
bot_user.remnawave_uuid = remna_user.get('uuid')
await db.update_user(bot_user)
updated += 1
synced += 1
except Exception as e:
logger.error(f"Error syncing user {remna_user.get('username')}: {e}")
errors += 1
result_text = (
f"✅ Синхронизация пользователей завершена!\n\n"
f"📊 Результаты:\n"
f"Всего пользователей в RemnaWave: {total_users}\n"
f"• Синхронизировано: {synced}\n"
f"• Создано новых: {created}\n"
f"• Обновлено: {updated}\n"
f"• Ошибок: {errors}"
)
await progress_msg.edit_text(
result_text,
reply_markup=sync_remnawave_keyboard(user.language)
)
log_user_action(user.telegram_id, "users_synced", f"Total: {total_users}, Synced: {synced}")
except Exception as e:
logger.error(f"Error in user sync: {e}")
await callback.message.edit_text(
f"❌ Ошибка синхронизации: {str(e)}",
reply_markup=sync_remnawave_keyboard(user.language)
)
@admin_router.callback_query(F.data == "sync_subscriptions_remnawave")
async def sync_subscriptions_remnawave_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, db: Database = None, **kwargs):
if not await check_admin_access(callback, user):
return
if not api or not db:
await callback.answer("❌ API или база данных недоступны", show_alert=True)
return
try:
await callback.answer("🔄 Запускаю улучшенную синхронизацию подписок...")
progress_msg = await callback.message.edit_text("⏳ Синхронизация подписок...\n\nЭтап 1/4: Получение данных...")
logger.info("=== STARTING SUBSCRIPTION SYNC ===")
remna_users = await api.get_all_system_users_full()
if not remna_users:
logger.error("No users returned from RemnaWave API")
await progress_msg.edit_text(
"Не удалось получить пользователей из RemnaWave",
reply_markup=sync_remnawave_keyboard(user.language)
)
return
logger.info(f"Got {len(remna_users)} total users from RemnaWave")
users_with_tg = [u for u in remna_users if u.get('telegramId')]
logger.info(f"Found {len(users_with_tg)} RemnaWave users with Telegram ID")
if users_with_tg:
first_user = users_with_tg[0]
logger.info(f"Sample user structure: {list(first_user.keys())}")
logger.info(f"Sample user: telegramId={first_user.get('telegramId')}, "
f"username={first_user.get('username')}, "
f"status={first_user.get('status')}, "
f"shortUuid={first_user.get('shortUuid')}, "
f"expireAt={first_user.get('expireAt')}")
created_subs = 0
updated_subs = 0
created_users = 0
updated_users = 0
errors = 0
await progress_msg.edit_text("⏳ Синхронизация подписок...\n\nЭтап 1/4: Создание пользователей...")
for i, remna_user in enumerate(users_with_tg):
try:
telegram_id = remna_user['telegramId']
logger.debug(f"Processing user {i+1}/{len(users_with_tg)}: {telegram_id}")
bot_user = await db.get_user_by_telegram_id(telegram_id)
if not bot_user:
is_admin = telegram_id in (kwargs.get('config', {}).ADMIN_IDS if 'config' in kwargs else [])
bot_user = await db.create_user(
telegram_id=telegram_id,
username=remna_user.get('username'),
first_name=remna_user.get('username'),
language='ru',
is_admin=is_admin
)
created_users += 1
logger.info(f"Created bot user for Telegram ID: {telegram_id}")
if not bot_user.remnawave_uuid and remna_user.get('uuid'):
bot_user.remnawave_uuid = remna_user['uuid']
await db.update_user(bot_user)
updated_users += 1
logger.debug(f"Updated RemnaWave UUID for user {telegram_id}")
except Exception as e:
logger.error(f"Error creating/updating user {telegram_id}: {e}")
errors += 1
logger.info(f"User creation phase: created={created_users}, updated={updated_users}, errors={errors}")
await progress_msg.edit_text("⏳ Синхронизация подписок...\n\nЭтап 2/4: Поиск подписок...")
for i, remna_user in enumerate(users_with_tg):
try:
telegram_id = remna_user['telegramId']
short_uuid = remna_user.get('shortUuid')
status = remna_user.get('status')
expire_at = remna_user.get('expireAt')
logger.debug(f"Syncing subscription for user {telegram_id}: "
f"shortUuid={short_uuid}, status={status}, expireAt={expire_at}")
bot_user = await db.get_user_by_telegram_id(telegram_id)
if not bot_user:
logger.warning(f"Bot user {telegram_id} not found during subscription sync")
continue
is_active_in_remna = status == 'ACTIVE'
has_expiry = bool(expire_at)
if not short_uuid:
logger.debug(f"User {telegram_id} has no shortUuid, skipping")
continue
existing_sub = await db.get_user_subscription_by_short_uuid(telegram_id, short_uuid)
if existing_sub:
logger.debug(f"Found existing subscription for user {telegram_id}")
if has_expiry:
try:
if remna_user['expireAt'].endswith('Z'):
expire_dt = datetime.fromisoformat(remna_user['expireAt'].replace('Z', '+00:00'))
else:
expire_dt = datetime.fromisoformat(remna_user['expireAt'])
expire_dt_naive = expire_dt.replace(tzinfo=None) if expire_dt.tzinfo else expire_dt
existing_sub.expires_at = expire_dt_naive
except Exception as date_error:
logger.error(f"Error parsing date for user {telegram_id}: {date_error}")
existing_sub.is_active = is_active_in_remna
if remna_user.get('trafficLimitBytes') is not None:
traffic_gb = remna_user['trafficLimitBytes'] // (1024 * 1024 * 1024) if remna_user['trafficLimitBytes'] > 0 else 0
existing_sub.traffic_limit_gb = traffic_gb
await db.update_user_subscription(existing_sub)
updated_subs += 1
else:
logger.debug(f"No existing subscription found for user {telegram_id}, creating new one")
if is_active_in_remna or has_expiry:
logger.info(f"Creating new subscription for user {telegram_id}")
squad_uuid = None
active_squads = remna_user.get('activeInternalSquads', [])
if active_squads:
first_squad = active_squads[0]
if isinstance(first_squad, dict):
squad_uuid = first_squad.get('uuid') or first_squad.get('id')
else:
squad_uuid = str(first_squad)
if not squad_uuid:
internal_squads = remna_user.get('internalSquads', [])
if internal_squads:
first_squad = internal_squads[0]
if isinstance(first_squad, dict):
squad_uuid = first_squad.get('uuid') or first_squad.get('id')
else:
squad_uuid = str(first_squad)
subscription_plan = None
if squad_uuid:
all_plans = await db.get_all_subscriptions(include_inactive=True, exclude_trial=False)
for plan in all_plans:
if plan.squad_uuid == squad_uuid:
subscription_plan = plan
break
if not subscription_plan:
traffic_gb = 0
if remna_user.get('trafficLimitBytes'):
traffic_gb = remna_user['trafficLimitBytes'] // (1024 * 1024 * 1024)
plan_name = f"Imported_{remna_user.get('username', 'User')[:10]}"
if squad_uuid:
plan_name += f"_{squad_uuid[:8]}"
subscription_plan = await db.create_subscription(
name=plan_name,
description=f"Автоматически импортированная подписка из RemnaWave",
price=0,
duration_days=30,
traffic_limit_gb=traffic_gb,
squad_uuid=squad_uuid or ''
)
logger.info(f"Created new subscription plan: {plan_name}")
expire_dt_naive = None
if has_expiry:
try:
if remna_user['expireAt'].endswith('Z'):
expire_dt = datetime.fromisoformat(remna_user['expireAt'].replace('Z', '+00:00'))
else:
expire_dt = datetime.fromisoformat(remna_user['expireAt'])
expire_dt_naive = expire_dt.replace(tzinfo=None) if expire_dt.tzinfo else expire_dt
except:
expire_dt_naive = datetime.now() + timedelta(days=30)
else:
expire_dt_naive = datetime.now() + timedelta(days=30)
user_subscription = await db.create_user_subscription(
user_id=telegram_id,
subscription_id=subscription_plan.id,
short_uuid=short_uuid,
expires_at=expire_dt_naive,
is_active=is_active_in_remna
)
if user_subscription:
created_subs += 1
logger.info(f"Created subscription for user {telegram_id} with short_uuid {short_uuid}")
else:
logger.error(f"Failed to create subscription for user {telegram_id}")
errors += 1
except Exception as e:
logger.error(f"Error syncing subscription for user {telegram_id}: {e}")
errors += 1
await progress_msg.edit_text("⏳ Синхронизация подписок...\n\nЭтап 3/4: Проверка консистентности...")
consistency_fixes = 0
for remna_user in users_with_tg:
try:
telegram_id = remna_user['telegramId']
user_subs = await db.get_user_subscriptions(telegram_id)
for user_sub in user_subs:
if user_sub.expires_at < datetime.now() and user_sub.is_active:
user_sub.is_active = False
await db.update_user_subscription(user_sub)
if remna_user.get('uuid'):
await api.update_user(remna_user['uuid'], {'status': 'EXPIRED'})
consistency_fixes += 1
except Exception as e:
logger.error(f"Error in consistency check for user {telegram_id}: {e}")
await progress_msg.edit_text("⏳ Синхронизация подписок...\n\nЭтап 4/4: Финальная проверка...")
total_bot_users = len(await db.get_all_users())
total_bot_subs = 0
active_bot_subs = 0
all_bot_users = await db.get_all_users()
for bot_user in all_bot_users:
user_subs = await db.get_user_subscriptions(bot_user.telegram_id)
total_bot_subs += len(user_subs)
active_bot_subs += len([s for s in user_subs if s.is_active])
result_text = (
"✅ Улучшенная синхронизация подписок завершена!\n\n"
"📊 Результаты синхронизации:\n\n"
"👥 Пользователи:\n"
f"• Создано в боте: {created_users}\n"
f"• Обновлено в боте: {updated_users}\n\n"
"📋 Подписки:\n"
f"• Создано новых: {created_subs}\n"
f"• Обновлено существующих: {updated_subs}\n"
f"• Исправлено несоответствий: {consistency_fixes}\n"
f"• Ошибок: {errors}\n\n"
"📈 Текущее состояние бота:\n"
f"Всего пользователей: {total_bot_users}\n"
f"Всего подписок: {total_bot_subs}\n"
f"• Активных подписок: {active_bot_subs}\n\n"
f"🕐 Завершено: {format_datetime(datetime.now(), user.language)}"
)
await progress_msg.edit_text(
result_text,
reply_markup=sync_remnawave_keyboard(user.language)
)
log_user_action(user.telegram_id, "improved_sync_completed",
f"Created: {created_subs}, Updated: {updated_subs}, Users: {created_users}")
except Exception as e:
logger.error(f"Error in improved subscription sync: {e}", exc_info=True)
await callback.message.edit_text(
f"❌ Ошибка улучшенной синхронизации\n\nДетали: {str(e)[:200]}",
reply_markup=sync_remnawave_keyboard(user.language)
)
@admin_router.callback_query(F.data.startswith("reset_user_traffic_"))
async def reset_user_traffic_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
try:
user_uuid = callback.data.replace("reset_user_traffic_", "")
if not api:
await callback.answer("❌ API недоступен", show_alert=True)
return
await callback.answer("🔄 Сбрасываю трафик пользователя...")
result = await api.reset_user_traffic(user_uuid)
if result:
await callback.answer("✅ Трафик пользователя успешно сброшен", show_alert=True)
log_user_action(user.telegram_id, "reset_user_traffic", f"UUID: {user_uuid}")
try:
updated_user = await api.get_user_by_uuid(user_uuid)
if updated_user:
used_traffic = updated_user.get('usedTrafficBytes', 0)
await callback.message.edit_reply_markup(
reply_markup=callback.message.reply_markup
)
except:
pass
else:
await callback.answer("❌ Ошибка сброса трафика", show_alert=True)
except Exception as e:
logger.error(f"Error resetting user traffic: {e}")
await callback.answer("❌ Ошибка операции", show_alert=True)
@admin_router.callback_query(F.data.startswith("disable_user_"))
async def disable_user_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
try:
user_uuid = callback.data.replace("disable_user_", "")
if not api:
await callback.answer("❌ API недоступен", show_alert=True)
return
await callback.answer("🔄 Отключаю пользователя...")
result = await api.disable_user(user_uuid)
if result:
await callback.answer("✅ Пользователь успешно отключен", show_alert=True)
log_user_action(user.telegram_id, "disable_user", f"UUID: {user_uuid}")
else:
await callback.answer("❌ Ошибка отключения пользователя", show_alert=True)
except Exception as e:
logger.error(f"Error disabling user: {e}")
await callback.answer("❌ Ошибка операции", show_alert=True)
@admin_router.callback_query(F.data.startswith("enable_user_"))
async def enable_user_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
try:
user_uuid = callback.data.replace("enable_user_", "")
if not api:
await callback.answer("❌ API недоступен", show_alert=True)
return
await callback.answer("🔄 Включаю пользователя...")
result = await api.enable_user(user_uuid)
if result:
await callback.answer("✅ Пользователь успешно включен", show_alert=True)
log_user_action(user.telegram_id, "enable_user", f"UUID: {user_uuid}")
else:
await callback.answer("❌ Ошибка включения пользователя", show_alert=True)
except Exception as e:
logger.error(f"Error enabling user: {e}")
await callback.answer("❌ Ошибка операции", show_alert=True)
@admin_router.callback_query(F.data == "sync_status_remnawave")
async def sync_status_remnawave_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, db: Database = None, **kwargs):
if not await check_admin_access(callback, user):
return
if not api or not db:
await callback.answer("❌ API или база данных недоступны", show_alert=True)
return
try:
await callback.answer("📊 Проверяю статус синхронизации...")
remna_users = await api.get_all_system_users_full()
bot_users = await db.get_all_users()
remna_with_tg = len([u for u in remna_users if u.get('telegramId')])
remna_without_tg = len(remna_users) - remna_with_tg
bot_with_uuid = len([u for u in bot_users if u.remnawave_uuid])
bot_without_uuid = len(bot_users) - bot_with_uuid
total_bot_subs = 0
synced_subs = 0
for bot_user in bot_users:
user_subs = await db.get_user_subscriptions(bot_user.telegram_id)
total_bot_subs += len(user_subs)
for user_sub in user_subs:
for remna_user in remna_users:
if remna_user.get('shortUuid') == user_sub.short_uuid:
synced_subs += 1
break
text = "📊 **Статус синхронизации**\n\n"
text += "RemnaWave:\n"
text += f"Всего пользователей: {len(remna_users)}\n"
text += f"С Telegram ID: {remna_with_tg}\n"
text += f"• Без Telegram ID: {remna_without_tg}\n\n"
text += "Бот:\n"
text += f"Всего пользователей: {len(bot_users)}\n"
text += f"С RemnaWave UUID: {bot_with_uuid}\n"
text += f"• Без RemnaWave UUID: {bot_without_uuid}\n\n"
text += "Подписки:\n"
text += f"Всего в боте: {total_bot_subs}\n"
text += f"• Синхронизировано: {synced_subs}\n"
text += f"Не синхронизировано: {total_bot_subs - synced_subs}\n\n"
if bot_without_uuid > 0 or remna_without_tg > 0 or (total_bot_subs - synced_subs) > 0:
text += "⚠️ Рекомендации:\n"
if bot_without_uuid > 0:
text += f"{bot_without_uuid} пользователей бота не связаны с RemnaWave\n"
if remna_without_tg > 0:
text += f"{remna_without_tg} пользователей RemnaWave не имеют Telegram ID\n"
if (total_bot_subs - synced_subs) > 0:
text += f"{total_bot_subs - synced_subs} подписок не синхронизированы\n"
text += "\n💡 Рекомендуется выполнить полную синхронизацию\n"
else:
text += "Все данные синхронизированы\n"
text += f"\n🕐 _Проверено: {format_datetime(datetime.now(), user.language)}_"
await callback.message.edit_text(
text,
reply_markup=sync_remnawave_keyboard(user.language)
)
except Exception as e:
logger.error(f"Error getting sync status: {e}")
await callback.message.edit_text(
f"❌ Ошибка получения статуса\n\n{str(e)[:200]}",
reply_markup=sync_remnawave_keyboard(user.language)
)
@admin_router.callback_query(F.data == "filter_users_active")
async def filter_users_active_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
if not api:
await callback.answer("❌ API недоступен", show_alert=True)
return
try:
await callback.answer("🔍 Фильтрую активных пользователей...")
all_users = await api.get_all_system_users_full()
active_users = [u for u in all_users if u.get('status') == 'ACTIVE']
if not active_users:
await callback.message.edit_text(
"❌ Активные пользователи не найдены",
reply_markup=system_users_keyboard(user.language)
)
return
text = f"✅ Активные пользователи ({len(active_users)})\n\n"
for i, sys_user in enumerate(active_users[:10], 1):
username = sys_user.get('username', 'N/A')
username = username.replace('*', '').replace('_', '').replace('[', '').replace(']', '').replace('`', '')
telegram_id = sys_user.get('telegramId', 'N/A')
short_uuid = sys_user.get('shortUuid', '')[:8] + "..."
text += f"{i}. {username}\n"
if telegram_id != 'N/A':
text += f" 📱 TG: {telegram_id}\n"
text += f" 🔗 {short_uuid}\n"
if sys_user.get('expireAt'):
expire_date = sys_user['expireAt'][:10]
text += f" ⏰ До {expire_date}\n"
text += "\n"
if len(active_users) > 10:
text += f"... и еще {len(active_users) - 10} активных пользователей"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Сбросить фильтр", callback_data="list_all_system_users")],
[InlineKeyboardButton(text="📱 С Telegram", callback_data="filter_users_telegram")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="system_users")]
])
await callback.message.edit_text(
text,
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Error filtering active users: {e}")
await callback.answer("❌ Ошибка фильтрации", show_alert=True)
@admin_router.callback_query(F.data == "filter_users_telegram")
async def filter_users_telegram_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
if not await check_admin_access(callback, user):
return
if not api:
await callback.answer("❌ API недоступен", show_alert=True)
return
try:
await callback.answer("🔍 Фильтрую пользователей с Telegram...")
all_users = await api.get_all_system_users_full()
tg_users = [u for u in all_users if u.get('telegramId')]
if not tg_users:
await callback.message.edit_text(
"❌ Пользователи с Telegram ID не найдены",
reply_markup=system_users_keyboard(user.language)
)
return
text = f"📱 **Пользователи с Telegram ID** ({len(tg_users)})\n\n"
for i, sys_user in enumerate(tg_users[:10], 1):
username = sys_user.get('username', 'N/A')
telegram_id = sys_user.get('telegramId')
status = sys_user.get('status', 'UNKNOWN')
status_emoji = "🟢" if status == 'ACTIVE' else "🔴"
text += f"{i}. {status_emoji} **{username}**\n"
text += f" 📱 TG: `{telegram_id}`\n"
if sys_user.get('shortUuid'):
text += f" 🔗 {sys_user['shortUuid'][:8]}...\n"
if sys_user.get('expireAt'):
expire_date = sys_user['expireAt'][:10]
text += f" ⏰ До {expire_date}\n"
text += "\n"
if len(tg_users) > 10:
text += f"_... и еще {len(tg_users) - 10} пользователей с Telegram_"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Сбросить фильтр", callback_data="list_all_system_users")],
[InlineKeyboardButton(text="✅ Только активные", callback_data="filter_users_active")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="system_users")]
])
await callback.message.edit_text(
text,
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Error filtering telegram users: {e}")
await callback.answer("❌ Ошибка фильтрации", show_alert=True)
@admin_router.callback_query(F.data == "show_all_nodes")
async def show_all_nodes_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, state: FSMContext = None, **kwargs):
if not await check_admin_access(callback, user):
return
if not api:
await callback.answer("❌ API недоступен", show_alert=True)
return
if state:
await state.clear()
await state.update_data(nodes_page=0)
await show_nodes_paginated(callback, user, api, state, page=0)
async def show_nodes_paginated(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None,
state: FSMContext = None, page: int = 0):
try:
nodes = await api.get_all_nodes()
if not nodes:
await callback.message.edit_text(
"❌ Ноды не найдены",
reply_markup=admin_system_keyboard(user.language)
)
return
nodes.sort(key=lambda x: (
0 if x.get('status') == 'online' else 1,
x.get('name', '')
))
nodes_per_page = 10
total_pages = (len(nodes) + nodes_per_page - 1) // nodes_per_page
start_idx = page * nodes_per_page
end_idx = min(start_idx + nodes_per_page, len(nodes))
page_nodes = nodes[start_idx:end_idx]
text = f"🖥 **Все ноды системы**\n"
text += f"📄 Страница {page + 1} из {total_pages}\n\n"
for i, node in enumerate(page_nodes, start=start_idx + 1):
status = node.get('status', 'unknown')
status_emoji = {
'online': '🟢',
'offline': '🔴',
'disabled': '',
'disconnected': '🔴',
'xray_stopped': '🟡'
}.get(status, '')
name = node.get('name', f'Node-{i}')
text += f"{i}. {status_emoji} **{name}**\n"
if node.get('address'):
text += f" 📍 {node['address'][:30]}...\n"
if node.get('cpuUsage') or node.get('memUsage'):
text += f" 💻 CPU: {node.get('cpuUsage', 0):.0f}% | RAM: {node.get('memUsage', 0):.0f}%\n"
if node.get('usersCount'):
text += f" 👥 Пользователей: {node['usersCount']}\n"
text += "\n"
buttons = []
if total_pages > 1:
nav_row = []
if page > 0:
nav_row.append(InlineKeyboardButton(text="◀️", callback_data=f"nodes_page_{page - 1}"))
nav_row.append(InlineKeyboardButton(text=f"{page + 1}/{total_pages}", callback_data="noop"))
if page < total_pages - 1:
nav_row.append(InlineKeyboardButton(text="▶️", callback_data=f"nodes_page_{page + 1}"))
buttons.append(nav_row)
buttons.append([
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_nodes_page_{page}")
])
buttons.append([
InlineKeyboardButton(text="🔙 Назад", callback_data="nodes_management")
])
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
await callback.message.edit_text(
text,
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Error showing nodes page: {e}")
await callback.answer("❌ Ошибка отображения", show_alert=True)
@admin_router.callback_query(F.data.startswith("nodes_page_"))
async def nodes_page_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, state: FSMContext = None, **kwargs):
if not await check_admin_access(callback, user):
return
try:
page = int(callback.data.split("_")[-1])
await show_nodes_paginated(callback, user, api, state, page)
except Exception as e:
logger.error(f"Error in nodes pagination: {e}")
await callback.answer("❌ Ошибка навигации", show_alert=True)
@admin_router.callback_query(F.data == "sync_full_remnawave")
async def sync_full_remnawave_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, db: Database = None, **kwargs):
if not await check_admin_access(callback, user):
return
if not api or not db:
await callback.answer("❌ API или база данных недоступны", show_alert=True)
return
try:
await callback.answer("🔄 Запускаю полную синхронизацию...")
progress_msg = await callback.message.edit_text(
"⏳ Полная синхронизация RemnaWave\n\n"
"Этап 1/5: Получение данных..."
)
remna_users = await api.get_all_system_users_full()
users_with_tg = [u for u in remna_users if u.get('telegramId')]
logger.info(f"Starting full sync for {len(users_with_tg)} users with Telegram ID")
users_created = 0
users_updated = 0
subs_created = 0
subs_updated = 0
plans_created = 0
statuses_updated = 0
errors = 0
await progress_msg.edit_text(
"⏳ Полная синхронизация RemnaWave\n\n"
"Этап 1/5: Синхронизация пользователей..."
)
for remna_user in users_with_tg:
try:
telegram_id = remna_user['telegramId']
bot_user = await db.get_user_by_telegram_id(telegram_id)
if not bot_user:
is_admin = telegram_id in (kwargs.get('config', {}).ADMIN_IDS if 'config' in kwargs else [])
bot_user = await db.create_user(
telegram_id=telegram_id,
username=remna_user.get('username'),
first_name=remna_user.get('username'),
language='ru',
is_admin=is_admin
)
users_created += 1
logger.info(f"Created user {telegram_id}")
if not bot_user.remnawave_uuid and remna_user.get('uuid'):
bot_user.remnawave_uuid = remna_user['uuid']
await db.update_user(bot_user)
users_updated += 1
except Exception as e:
logger.error(f"Error syncing user {telegram_id}: {e}")
errors += 1
await progress_msg.edit_text(
"⏳ Полная синхронизация RemnaWave\n\n"
"Этап 2/5: Создание планов подписок..."
)
unique_squads = set()
for remna_user in users_with_tg:
active_squads = remna_user.get('activeInternalSquads', [])
internal_squads = remna_user.get('internalSquads', [])
for squad_list in [active_squads, internal_squads]:
for squad in squad_list:
if isinstance(squad, dict):
squad_uuid = squad.get('uuid') or squad.get('id')
else:
squad_uuid = str(squad)
if squad_uuid:
unique_squads.add(squad_uuid)
logger.info(f"Found {len(unique_squads)} unique squads")
existing_plans = await db.get_all_subscriptions(include_inactive=True, exclude_trial=False)
existing_squad_uuids = {plan.squad_uuid for plan in existing_plans if plan.squad_uuid}
for squad_uuid in unique_squads:
if squad_uuid not in existing_squad_uuids:
try:
plan_name = f"Auto_Squad_{squad_uuid[:8]}"
new_plan = await db.create_subscription(
name=plan_name,
description=f"Автоматически созданный план для squad {squad_uuid}",
price=0,
duration_days=30,
traffic_limit_gb=0,
squad_uuid=squad_uuid
)
plans_created += 1
logger.info(f"Created subscription plan for squad {squad_uuid}")
except Exception as e:
logger.error(f"Error creating plan for squad {squad_uuid}: {e}")
errors += 1
await progress_msg.edit_text(
"⏳ Полная синхронизация RemnaWave\n\n"
"Этап 3/5: Синхронизация подписок..."
)
for remna_user in users_with_tg:
try:
telegram_id = remna_user['telegramId']
short_uuid = remna_user.get('shortUuid')
if not short_uuid:
continue
existing_sub = await db.get_user_subscription_by_short_uuid(telegram_id, short_uuid)
if existing_sub:
if remna_user.get('expireAt'):
try:
expire_dt = datetime.fromisoformat(remna_user['expireAt'].replace('Z', '+00:00'))
existing_sub.expires_at = expire_dt.replace(tzinfo=None)
except:
pass
existing_sub.is_active = remna_user.get('status') == 'ACTIVE'
await db.update_user_subscription(existing_sub)
subs_updated += 1
else:
if remna_user.get('status') == 'ACTIVE' or remna_user.get('expireAt'):
squad_uuid = None
active_squads = remna_user.get('activeInternalSquads', [])
if active_squads:
first_squad = active_squads[0]
if isinstance(first_squad, dict):
squad_uuid = first_squad.get('uuid') or first_squad.get('id')
else:
squad_uuid = str(first_squad)
subscription_plan = None
all_plans = await db.get_all_subscriptions(include_inactive=True, exclude_trial=False)
for plan in all_plans:
if plan.squad_uuid == squad_uuid:
subscription_plan = plan
break
if subscription_plan:
expire_dt = None
if remna_user.get('expireAt'):
try:
expire_dt = datetime.fromisoformat(remna_user['expireAt'].replace('Z', '+00:00'))
expire_dt = expire_dt.replace(tzinfo=None)
except:
expire_dt = datetime.now() + timedelta(days=30)
else:
expire_dt = datetime.now() + timedelta(days=30)
user_sub = await db.create_user_subscription(
user_id=telegram_id,
subscription_id=subscription_plan.id,
short_uuid=short_uuid,
expires_at=expire_dt,
is_active=remna_user.get('status') == 'ACTIVE'
)
if user_sub:
subs_created += 1
logger.info(f"Created subscription for user {telegram_id}")
except Exception as e:
logger.error(f"Error syncing subscription for user {telegram_id}: {e}")
errors += 1
await progress_msg.edit_text(
"⏳ Полная синхронизация RemnaWave\n\n"
"Этап 4/5: Обновление статусов..."
)
all_bot_users = await db.get_all_users()
for bot_user in all_bot_users:
user_subs = await db.get_user_subscriptions(bot_user.telegram_id)
for user_sub in user_subs:
if user_sub.expires_at < datetime.now() and user_sub.is_active:
user_sub.is_active = False
await db.update_user_subscription(user_sub)
statuses_updated += 1
if bot_user.remnawave_uuid:
try:
await api.update_user(bot_user.remnawave_uuid, {'status': 'EXPIRED'})
except:
pass
await progress_msg.edit_text(
"⏳ Полная синхронизация RemnaWave\n\n"
"Этап 5/5: Подсчет результатов..."
)
total_bot_users = len(await db.get_all_users())
total_subscriptions = 0
active_subscriptions = 0
for bot_user in all_bot_users:
user_subs = await db.get_user_subscriptions(bot_user.telegram_id)
total_subscriptions += len(user_subs)
active_subscriptions += len([s for s in user_subs if s.is_active])
result_text = (
"✅ Полная синхронизация завершена!\n\n"
"📊 Результаты операции:\n\n"
"👥 Пользователи:\n"
f"• Создано: {users_created}\n"
f"• Обновлено: {users_updated}\n\n"
"📋 Планы подписок:\n"
f"• Создано новых планов: {plans_created}\n\n"
"🎫 Подписки:\n"
f"• Создано: {subs_created}\n"
f"• Обновлено: {subs_updated}\n\n"
"🔄 Статусы:\n"
f"• Обновлено: {statuses_updated}\n"
f"• Ошибок: {errors}\n\n"
"📈 Текущее состояние:\n"
f"• Пользователей в боте: {total_bot_users}\n"
f"Всего подписок: {total_subscriptions}\n"
f"• Активных подписок: {active_subscriptions}\n\n"
f"🕐 Завершено: {format_datetime(datetime.now(), user.language)}"
)
await progress_msg.edit_text(
result_text,
reply_markup=sync_remnawave_keyboard(user.language)
)
log_user_action(user.telegram_id, "full_sync_improved_completed",
f"Users: {users_created}/{users_updated}, Subs: {subs_created}/{subs_updated}, Plans: {plans_created}")
except Exception as e:
logger.error(f"Error in improved full sync: {e}", exc_info=True)
await callback.message.edit_text(
f"❌ Ошибка полной синхронизации\n\nДетали: {str(e)[:200]}",
reply_markup=sync_remnawave_keyboard(user.language)
)
@admin_router.callback_query(F.data == "sync_single_user")
async def sync_single_user_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
if not await check_admin_access(callback, user):
return
await callback.message.edit_text(
"👤 Синхронизация конкретного пользователя\n\n"
"Введите Telegram ID пользователя для синхронизации:",
reply_markup=cancel_keyboard(user.language)
)
await state.set_state(BotStates.admin_sync_single_user)
@admin_router.message(StateFilter(BotStates.admin_sync_single_user))
async def handle_sync_single_user(message: Message, state: FSMContext, user: User,
api: RemnaWaveAPI = None, db: Database = None, **kwargs):
if not api or not db:
await message.answer("❌ API или база данных недоступны")
await state.clear()
return
try:
telegram_id = int(message.text.strip())
except ValueError:
await message.answer("❌ Неверный формат Telegram ID")
return
try:
progress_msg = await message.answer("🔄 Синхронизирую пользователя...")
remna_user_result = await api.get_user_by_telegram_id(telegram_id)
logger.info(f"API result type: {type(remna_user_result)}")
logger.info(f"API result: {remna_user_result}")
remna_user = None
if isinstance(remna_user_result, dict):
remna_user = remna_user_result
elif isinstance(remna_user_result, list):
if remna_user_result:
remna_user = remna_user_result[0]
else:
remna_user = None
else:
remna_user = None
if not remna_user or not isinstance(remna_user, dict):
await progress_msg.edit_text(
f"❌ Пользователь с Telegram ID {telegram_id} не найден в RemnaWave\n\n"
f"Тип ответа API: {type(remna_user_result)}\n"
f"Содержимое: {str(remna_user_result)[:100]}...",
reply_markup=admin_menu_keyboard(user.language)
)
await state.clear()
return
result_details = []
bot_user = await db.get_user_by_telegram_id(telegram_id)
if not bot_user:
is_admin = telegram_id in (kwargs.get('config', {}).ADMIN_IDS if 'config' in kwargs else [])
bot_user = await db.create_user(
telegram_id=telegram_id,
username=remna_user.get('username'),
first_name=remna_user.get('username'),
language='ru',
is_admin=is_admin
)
result_details.append("✅ Создан пользователь в боте")
else:
result_details.append(" Пользователь уже существует в боте")
if not bot_user.remnawave_uuid and remna_user.get('uuid'):
bot_user.remnawave_uuid = remna_user['uuid']
await db.update_user(bot_user)
result_details.append("✅ Обновлен RemnaWave UUID")
short_uuid = remna_user.get('shortUuid')
if short_uuid:
existing_sub = await db.get_user_subscription_by_short_uuid(telegram_id, short_uuid)
if existing_sub:
if remna_user.get('expireAt'):
try:
expire_str = remna_user['expireAt']
if expire_str.endswith('Z'):
expire_dt = datetime.fromisoformat(expire_str.replace('Z', '+00:00'))
else:
expire_dt = datetime.fromisoformat(expire_str)
existing_sub.expires_at = expire_dt.replace(tzinfo=None)
existing_sub.is_active = remna_user.get('status') == 'ACTIVE'
await db.update_user_subscription(existing_sub)
result_details.append("✅ Обновлена существующая подписка")
except Exception as e:
result_details.append(f"❌ Ошибка обновления подписки: {str(e)[:50]}")
logger.error(f"Error updating subscription: {e}")
else:
if remna_user.get('status') == 'ACTIVE' or remna_user.get('expireAt'):
squad_uuid = None
active_squads = remna_user.get('activeInternalSquads', [])
if active_squads and isinstance(active_squads, list):
first_squad = active_squads[0]
if isinstance(first_squad, dict):
squad_uuid = first_squad.get('uuid') or first_squad.get('id')
elif isinstance(first_squad, str):
squad_uuid = first_squad
if not squad_uuid:
internal_squads = remna_user.get('internalSquads', [])
if internal_squads and isinstance(internal_squads, list):
first_squad = internal_squads[0]
if isinstance(first_squad, dict):
squad_uuid = first_squad.get('uuid') or first_squad.get('id')
elif isinstance(first_squad, str):
squad_uuid = first_squad
subscription_plan = None
if squad_uuid:
all_plans = await db.get_all_subscriptions(include_inactive=True, exclude_trial=False)
for plan in all_plans:
if plan.squad_uuid == squad_uuid:
subscription_plan = plan
break
if not subscription_plan and squad_uuid:
traffic_gb = 0
if remna_user.get('trafficLimitBytes'):
traffic_gb = remna_user['trafficLimitBytes'] // (1024 * 1024 * 1024)
subscription_plan = await db.create_subscription(
name=f"Auto_{remna_user.get('username', 'User')[:10]}",
description=f"Автоматически созданный план для {remna_user.get('username')}",
price=0,
duration_days=30,
traffic_limit_gb=traffic_gb,
squad_uuid=squad_uuid
)
result_details.append("✅ Создан новый план подписки")
if subscription_plan:
expire_dt = datetime.now() + timedelta(days=30)
if remna_user.get('expireAt'):
try:
expire_str = remna_user['expireAt']
if expire_str.endswith('Z'):
expire_dt = datetime.fromisoformat(expire_str.replace('Z', '+00:00'))
else:
expire_dt = datetime.fromisoformat(expire_str)
expire_dt = expire_dt.replace(tzinfo=None)
except Exception as date_error:
logger.error(f"Error parsing date {remna_user.get('expireAt')}: {date_error}")
user_sub = await db.create_user_subscription(
user_id=telegram_id,
subscription_id=subscription_plan.id,
short_uuid=short_uuid,
expires_at=expire_dt,
is_active=remna_user.get('status') == 'ACTIVE'
)
if user_sub:
result_details.append("✅ Создана новая подписка")
else:
result_details.append("❌ Ошибка создания подписки")
else:
result_details.append(f"Не удалось найти или создать план подписки (squad_uuid: {squad_uuid})")
else:
result_details.append(" Пользователь неактивен или нет срока действия")
else:
result_details.append(" У пользователя нет short_uuid")
status_emoji = "🟢" if remna_user.get('status') == 'ACTIVE' else "🔴"
username = remna_user.get('username', 'N/A')
report_text = f"👤 Синхронизация пользователя завершена\n\n"
report_text += f"Пользователь: {status_emoji} {username}\n"
report_text += f"Telegram ID: {telegram_id}\n"
report_text += f"Статус в RemnaWave: {remna_user.get('status', 'N/A')}\n"
report_text += f"UUID: {remna_user.get('uuid', 'N/A')[:20]}...\n"
report_text += f"Short UUID: {remna_user.get('shortUuid', 'N/A')}\n"
if remna_user.get('expireAt'):
expire_date = remna_user['expireAt'][:10]
report_text += f"Действует до: {expire_date}\n"
active_squads = remna_user.get('activeInternalSquads', [])
if active_squads:
report_text += f"Активных squad: {len(active_squads)}\n"
report_text += f"\n📋 Выполненные действия:\n"
for detail in result_details:
report_text += f"{detail}\n"
await progress_msg.edit_text(
report_text,
reply_markup=admin_menu_keyboard(user.language)
)
log_user_action(user.telegram_id, "single_user_synced", f"User: {telegram_id}")
except Exception as e:
logger.error(f"Error syncing single user: {e}", exc_info=True)
await message.answer(
f"❌ Ошибка синхронизации\n\nДетали: {str(e)[:100]}",
reply_markup=admin_menu_keyboard(user.language)
)
await state.clear()
@admin_router.callback_query(F.data == "import_all_by_telegram")
async def import_all_by_telegram_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, db: Database = None, **kwargs):
if not await check_admin_access(callback, user):
return
if not api or not db:
await callback.answer("❌ API или база данных недоступны", show_alert=True)
return
try:
await callback.answer("🔄 Запускаю массовый импорт всех подписок...")
progress_msg = await callback.message.edit_text(
"⏳ Массовый импорт подписок по Telegram ID\n\n"
"Этап 1/5: Получение всех записей из RemnaWave..."
)
all_remna_records = await api.get_all_system_users_full()
if not all_remna_records:
await progress_msg.edit_text(
"Не удалось получить записи из RemnaWave",
reply_markup=sync_remnawave_keyboard(user.language)
)
return
logger.info(f"Got {len(all_remna_records)} total records from RemnaWave")
records_with_telegram = [r for r in all_remna_records if r.get('telegramId')]
logger.info(f"Found {len(records_with_telegram)} records with Telegram ID")
users_by_telegram = {}
for record in records_with_telegram:
tg_id = record['telegramId']
if tg_id not in users_by_telegram:
users_by_telegram[tg_id] = []
users_by_telegram[tg_id].append(record)
logger.info(f"Found {len(users_by_telegram)} unique Telegram users with {len(records_with_telegram)} total subscriptions")
bot_users_created = 0
bot_users_updated = 0
plans_created = 0
subscriptions_imported = 0
subscriptions_updated = 0
errors = 0
skipped_no_shortuid = 0
await progress_msg.edit_text(
"⏳ Массовый импорт подписок по Telegram ID\n\n"
"Этап 1/5: Создание пользователей бота..."
)
for telegram_id, user_records in users_by_telegram.items():
try:
logger.info(f"Processing Telegram user {telegram_id} with {len(user_records)} subscriptions")
latest_record = max(user_records, key=lambda x: x.get('updatedAt', x.get('createdAt', '')))
bot_user = await db.get_user_by_telegram_id(telegram_id)
if not bot_user:
is_admin = telegram_id in (kwargs.get('config', {}).ADMIN_IDS if 'config' in kwargs else [])
best_username = None
for record in user_records:
username = record.get('username', '')
if username and not username.startswith('user_'):
best_username = username
break
if not best_username:
best_username = latest_record.get('username', f"User_{telegram_id}")
bot_user = await db.create_user(
telegram_id=telegram_id,
username=best_username,
first_name=best_username,
language='ru',
is_admin=is_admin
)
bot_users_created += 1
logger.info(f"Created bot user for TG {telegram_id} with username {best_username}")
if latest_record.get('uuid') and bot_user.remnawave_uuid != latest_record['uuid']:
bot_user.remnawave_uuid = latest_record['uuid']
await db.update_user(bot_user)
bot_users_updated += 1
except Exception as e:
logger.error(f"Error processing Telegram user {telegram_id}: {e}")
errors += 1
await progress_msg.edit_text(
"⏳ Массовый импорт подписок по Telegram ID\n\n"
"Этап 2/5: Анализ squad'ов..."
)
all_squads = set()
squad_names = {}
for i, record in enumerate(records_with_telegram):
logger.debug(f"Analyzing record {i+1}/{len(records_with_telegram)}: {record.get('username')}")
active_squads = record.get('activeInternalSquads', [])
if active_squads and isinstance(active_squads, list):
for squad in active_squads:
if isinstance(squad, dict):
squad_uuid = squad.get('uuid')
squad_name = squad.get('name', 'Unknown Squad')
if squad_uuid:
all_squads.add(squad_uuid)
squad_names[squad_uuid] = squad_name
logger.debug(f"Found squad: {squad_uuid} ({squad_name})")
logger.info(f"Found {len(all_squads)} unique squad UUIDs: {list(all_squads)}")
await progress_msg.edit_text(
"⏳ Массовый импорт подписок по Telegram ID\n\n"
"Этап 3/5: Создание планов подписок..."
)
existing_plans = await db.get_all_subscriptions_admin()
existing_squad_uuids = {plan.squad_uuid for plan in existing_plans if plan.squad_uuid}
logger.info(f"Existing squad UUIDs in DB: {existing_squad_uuids}")
for squad_uuid in all_squads:
if squad_uuid not in existing_squad_uuids:
try:
squad_name = squad_names.get(squad_uuid, "Unknown Squad")
plan_name = f"Import_{squad_name[:15]}_{squad_uuid[:8]}"
logger.info(f"Creating plan for squad {squad_uuid}: {plan_name}")
new_plan = await db.create_subscription(
name="Старая подписка",
description=f"Импортированная подписка из RemnaWave (squad: {squad_name})",
price=0,
duration_days=30,
traffic_limit_gb=0,
squad_uuid=squad_uuid,
is_imported=True
)
plans_created += 1
logger.info(f"✅ Created plan for squad {squad_uuid}: {plan_name}")
except Exception as e:
logger.error(f"❌ Error creating plan for squad {squad_uuid}: {e}")
errors += 1
else:
logger.info(f"Plan for squad {squad_uuid} already exists")
await progress_msg.edit_text(
"⏳ Массовый импорт подписок по Telegram ID\n\n"
"Этап 4/5: Импорт подписок..."
)
for i, record in enumerate(records_with_telegram):
try:
telegram_id = record['telegramId']
short_uuid = record.get('shortUuid')
status = record.get('status', 'UNKNOWN')
expire_at = record.get('expireAt')
username = record.get('username')
logger.info(f"=== IMPORTING SUBSCRIPTION {i+1}/{len(records_with_telegram)} ===")
logger.info(f"TG={telegram_id}, Username={username}, shortUuid={short_uuid}, status={status}")
if not short_uuid:
skipped_no_shortuid += 1
logger.warning(f"❌ Skipping record: no shortUuid")
continue
existing_sub = await db.get_user_subscription_by_short_uuid(telegram_id, short_uuid)
if existing_sub:
existing_plan = await db.get_subscription_by_id(existing_sub.subscription_id)
if existing_plan:
logger.info(f"Updating existing subscription for TG {telegram_id}, shortUuid {short_uuid}")
if expire_at:
try:
if expire_at.endswith('Z'):
expire_dt = datetime.fromisoformat(expire_at.replace('Z', '+00:00'))
else:
expire_dt = datetime.fromisoformat(expire_at)
existing_sub.expires_at = expire_dt.replace(tzinfo=None)
except Exception as date_error:
logger.error(f"Error parsing date: {date_error}")
existing_sub.is_active = (status == 'ACTIVE')
if record.get('trafficLimitBytes') is not None:
traffic_gb = record['trafficLimitBytes'] // (1024 * 1024 * 1024) if record['trafficLimitBytes'] > 0 else 0
existing_sub.traffic_limit_gb = traffic_gb
await db.update_user_subscription(existing_sub)
subscriptions_updated += 1
else:
logger.warning(f"Found orphaned subscription {existing_sub.id} for user {telegram_id}, deleting...")
await db.delete_user_subscription(existing_sub.id)
logger.info(f"Creating new subscription after cleaning orphaned one")
existing_sub = None
if not existing_sub:
logger.info(f"Creating new subscription for TG {telegram_id}, shortUuid {short_uuid}")
squad_uuid = None
active_squads = record.get('activeInternalSquads', [])
if active_squads and isinstance(active_squads, list) and len(active_squads) > 0:
first_squad = active_squads[0]
if isinstance(first_squad, dict):
squad_uuid = first_squad.get('uuid')
logger.info(f"Extracted squad_uuid: {squad_uuid}")
if not squad_uuid:
logger.warning(f"❌ No squad_uuid found for record {username}")
errors += 1
continue
all_plans = await db.get_all_subscriptions_admin()
subscription_plan = None
for plan in all_plans:
if plan.squad_uuid == squad_uuid:
subscription_plan = plan
logger.info(f"✅ Found matching plan: {plan.name}")
break
if not subscription_plan:
logger.error(f"❌ No subscription plan found for squad {squad_uuid}")
errors += 1
continue
expire_dt_naive = datetime.now() + timedelta(days=30) # Дефолт
if expire_at:
try:
if expire_at.endswith('Z'):
expire_dt = datetime.fromisoformat(expire_at.replace('Z', '+00:00'))
else:
expire_dt = datetime.fromisoformat(expire_at)
expire_dt_naive = expire_dt.replace(tzinfo=None)
except Exception as date_error:
logger.error(f"Error parsing expiry date: {date_error}")
traffic_gb = 0
if record.get('trafficLimitBytes'):
traffic_gb = record['trafficLimitBytes'] // (1024 * 1024 * 1024)
user_subscription = await db.create_user_subscription(
user_id=telegram_id,
subscription_id=subscription_plan.id,
short_uuid=short_uuid,
expires_at=expire_dt_naive,
is_active=(status == 'ACTIVE'),
traffic_limit_gb=traffic_gb
)
if user_subscription:
subscriptions_imported += 1
logger.info(f"✅ Successfully imported subscription: TG={telegram_id}, shortUuid={short_uuid}")
else:
logger.error(f"❌ Failed to create subscription for TG {telegram_id}")
errors += 1
except Exception as e:
logger.error(f"❌ Error importing subscription for record {i+1}: {e}")
errors += 1
await progress_msg.edit_text(
"⏳ Массовый импорт подписок по Telegram ID\n\n"
"Этап 5/5: Подсчет результатов..."
)
final_bot_users = len(await db.get_all_users())
final_subscriptions = 0
final_active_subs = 0
all_bot_users = await db.get_all_users()
for bot_user in all_bot_users:
user_subs = await db.get_user_subscriptions(bot_user.telegram_id)
final_subscriptions += len(user_subs)
final_active_subs += len([s for s in user_subs if s.is_active])
result_text = (
"✅ Массовый импорт подписок завершен!\n\n"
"📊 Результаты импорта:\n\n"
"👥 Пользователи Telegram:\n"
f"• Уникальных пользователей: {len(users_by_telegram)}\n"
f"• Создано в боте: {bot_users_created}\n"
f"• Обновлено UUID: {bot_users_updated}\n\n"
"📋 Планы подписок:\n"
f"• Создано новых планов: {plans_created}\n\n"
"🎫 Подписки:\n"
f"Всего записей обработано: {len(records_with_telegram)}\n"
f"• Импортировано новых: {subscriptions_imported}\n"
f"• Обновлено существующих: {subscriptions_updated}\n"
f"• Пропущено (нет shortUuid): {skipped_no_shortuid}\n"
f"• Ошибок: {errors}\n\n"
"📈 Итоговая статистика бота:\n"
f"• Пользователей в боте: {final_bot_users}\n"
f"Всего подписок: {final_subscriptions}\n"
f"• Активных подписок: {final_active_subs}\n\n"
f"🕐 Завершено: {format_datetime(datetime.now(), user.language)}"
)
await progress_msg.edit_text(
result_text,
reply_markup=sync_remnawave_keyboard(user.language)
)
log_user_action(user.telegram_id, "bulk_import_completed",
f"Records: {len(records_with_telegram)}, Imported: {subscriptions_imported}, Updated: {subscriptions_updated}")
except Exception as e:
logger.error(f"Error in bulk import: {e}", exc_info=True)
await callback.message.edit_text(
f"❌ Ошибка массового импорта\n\nДетали: {str(e)[:200]}",
reply_markup=sync_remnawave_keyboard(user.language)
)
@admin_router.message(StateFilter(BotStates.admin_debug_user_structure))
async def handle_debug_user_structure(message: Message, state: FSMContext, user: User, api: RemnaWaveAPI = None, **kwargs):
if not api:
await message.answer("❌ API недоступен")
await state.clear()
return
try:
telegram_id = int(message.text.strip())
except ValueError:
await message.answer("❌ Неверный формат Telegram ID")
return
try:
remna_user = await api.get_user_by_telegram_id(telegram_id)
if not remna_user:
await message.answer(
f"❌ Пользователь с Telegram ID {telegram_id} не найден",
reply_markup=admin_menu_keyboard(user.language)
)
await state.clear()
return
analysis = f"🔍 Структура пользователя {telegram_id}\n\n"
analysis += "📋 Основные поля:\n"
for key in ['uuid', 'username', 'shortUuid', 'status', 'expireAt', 'telegramId']:
value = remna_user.get(key, 'N/A')
analysis += f"{key}: {value}\n"
analysis += "\n"
analysis += "🏷 Squad поля:\n"
squad_fields = ['activeInternalSquads', 'internalSquads', 'squads', 'squad', 'squadUuid', 'squadId']
for field in squad_fields:
if field in remna_user:
value = remna_user[field]
analysis += f"{field}: {value}\n"
if isinstance(value, list) and value:
for i, item in enumerate(value):
analysis += f" [{i}]: {item}\n"
if isinstance(item, dict):
for sub_key, sub_value in item.items():
analysis += f" {sub_key}: {sub_value}\n"
else:
analysis += f"{field}: ОТСУТСТВУЕТ\n"
analysis += "\n"
analysis += "📝 Все поля пользователя:\n"
for key, value in remna_user.items():
if key not in ['uuid', 'username', 'shortUuid', 'status', 'expireAt', 'telegramId'] + squad_fields:
if isinstance(value, str) and len(value) > 50:
value = value[:47] + "..."
analysis += f"{key}: {value}\n"
if len(analysis) > 4000:
parts = [analysis[i:i+4000] for i in range(0, len(analysis), 4000)]
for i, part in enumerate(parts):
if i == 0:
await message.answer(part)
else:
await message.answer(f"Часть {i+1}:\n{part}")
else:
await message.answer(analysis)
await message.answer(
"✅ Анализ завершен",
reply_markup=admin_menu_keyboard(user.language)
)
except Exception as e:
logger.error(f"Error debugging user structure: {e}")
await message.answer(
f"❌ Ошибка анализа: {str(e)[:100]}",
reply_markup=admin_menu_keyboard(user.language)
)
await state.clear()
@admin_router.callback_query(F.data == "rename_imported_plans")
async def rename_imported_plans_callback(callback: CallbackQuery, user: User, db: Database = None, state: FSMContext = None, **kwargs):
if not await check_admin_access(callback, user):
return
if not db:
await callback.answer("❌ База данных недоступна", show_alert=True)
return
try:
await callback.answer("🔄 Переименовываю импортированные планы...")
progress_msg = await callback.message.edit_text(
"⏳ Поиск и переименование импортированных планов..."
)
all_plans = await db.get_all_subscriptions_admin()
imported_plans = []
for plan in all_plans:
if plan.name == "Старая подписка":
continue
if getattr(plan, 'is_trial', False):
logger.debug(f"Skipping trial plan: {plan.name}")
continue
is_imported_plan = False
if getattr(plan, 'is_imported', False):
is_imported_plan = True
logger.debug(f"Plan {plan.name} marked as imported")
elif plan.name.startswith(('Import_', 'Auto_', 'Imported_')):
is_imported_plan = True
logger.debug(f"Plan {plan.name} has import prefix")
elif plan.name.startswith('Trial_') and not getattr(plan, 'is_trial', False):
is_imported_plan = True
logger.debug(f"Plan {plan.name} looks like imported trial")
elif (plan.price == 0 and
any(keyword in plan.name.lower() for keyword in ['user_', 'default', 'squad']) and
not getattr(plan, 'is_trial', False)):
is_imported_plan = True
logger.debug(f"Plan {plan.name} has suspicious import characteristics")
elif (plan.description and
'squad' in plan.description.lower() and
not getattr(plan, 'is_trial', False)):
is_imported_plan = True
logger.debug(f"Plan {plan.name} has squad in description")
if is_imported_plan:
imported_plans.append(plan)
logger.info(f"Found imported plan: {plan.name} (is_trial: {getattr(plan, 'is_trial', False)})")
logger.info(f"Found {len(imported_plans)} plans that look imported")
if not imported_plans:
await progress_msg.edit_text(
" Планы для переименования не найдены\n\n"
"Все импортированные планы уже имеют название 'Старая подписка'",
reply_markup=sync_remnawave_keyboard(user.language)
)
return
plans_list = []
for plan in imported_plans[:10]:
squad_short = plan.squad_uuid[:8] + "..." if plan.squad_uuid else "No Squad"
plans_list.append(f"{plan.name} ({squad_short})")
if len(imported_plans) > 10:
plans_list.append(f"... и еще {len(imported_plans) - 10}")
confirmation_text = (
f"🔍 Найдено {len(imported_plans)} планов для переименования:\n\n" +
"\n".join(plans_list) +
f"\n\n⚠️ Все эти планы будут переименованы в 'Старая подписка'"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Переименовать", callback_data="confirm_rename_plans"),
InlineKeyboardButton(text="❌ Отмена", callback_data="view_imported_plans")
]
])
if state:
plan_ids = [plan.id for plan in imported_plans]
await state.update_data(plans_to_rename=plan_ids)
await state.set_state(BotStates.admin_rename_plans_confirm)
await progress_msg.edit_text(confirmation_text, reply_markup=keyboard)
except Exception as e:
logger.error(f"Error finding imported plans: {e}")
await callback.message.edit_text(
f"❌ Ошибка поиска планов\n\n{str(e)[:200]}",
reply_markup=sync_remnawave_keyboard(user.language)
)
@admin_router.callback_query(F.data == "confirm_rename_plans", StateFilter(BotStates.admin_rename_plans_confirm))
async def confirm_rename_plans_callback(callback: CallbackQuery, user: User, db: Database = None, state: FSMContext = None, **kwargs):
if not await check_admin_access(callback, user):
return
if not db or not state:
await callback.answer("❌ База данных или состояние недоступны", show_alert=True)
return
try:
await callback.answer("🔄 Переименовываю планы...")
progress_msg = await callback.message.edit_text("⏳ Переименование планов...")
state_data = await state.get_data()
plan_ids = state_data.get('plans_to_rename', [])
if not plan_ids:
await progress_msg.edit_text(
"❌ Список планов для переименования потерян",
reply_markup=sync_remnawave_keyboard(user.language)
)
await state.clear()
return
renamed_count = 0
errors = 0
renamed_plans = []
for plan_id in plan_ids:
try:
plan = await db.get_subscription_by_id(plan_id)
if not plan:
continue
old_name = plan.name
plan.name = "Старая подписка"
plan.description = f"Импортированная подписка из RemnaWave (было: {old_name})"
plan.is_imported = True
await db.update_subscription(plan)
renamed_count += 1
renamed_plans.append(f"'{old_name}' -> 'Старая подписка'")
logger.info(f"Renamed plan: '{old_name}' -> 'Старая подписка'")
except Exception as e:
logger.error(f"Error renaming plan {plan_id}: {e}")
errors += 1
await state.clear()
result_text = (
f"✅ Переименование завершено!\n\n"
f"📊 Результаты:\n"
f"• Переименовано планов: {renamed_count}\n"
f"• Ошибок: {errors}\n\n"
f"🏷 Все планы теперь называются: 'Старая подписка'\n\n"
f"🕐 Завершено: {format_datetime(datetime.now(), user.language)}"
)
if renamed_count <= 5 and renamed_plans:
result_text += f"\n📋 Переименованные планы:\n" + "\n".join(f"{plan}" for plan in renamed_plans)
await progress_msg.edit_text(
result_text,
reply_markup=sync_remnawave_keyboard(user.language)
)
log_user_action(user.telegram_id, "renamed_imported_plans", f"Renamed: {renamed_count}")
except Exception as e:
logger.error(f"Error confirming rename plans: {e}")
await callback.message.edit_text(
f"❌ Ошибка переименования планов\n\n{str(e)[:200]}",
reply_markup=sync_remnawave_keyboard(user.language)
)
await state.clear()
@admin_router.callback_query(F.data == "view_imported_plans", StateFilter(BotStates.admin_rename_plans_confirm))
async def cancel_rename_plans(callback: CallbackQuery, state: FSMContext, user: User, **kwargs):
"""Cancel rename operation"""
await state.clear()
await view_imported_plans_callback(callback, user, **kwargs)
@admin_router.callback_query(F.data == "main_menu", StateFilter(BotStates.admin_rename_plans_confirm))
async def cancel_rename_to_main(callback: CallbackQuery, state: FSMContext, user: User, **kwargs):
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.callback_query(F.data == "view_imported_plans")
async def view_imported_plans_callback(callback: CallbackQuery, user: User, db: Database = None, **kwargs):
if not await check_admin_access(callback, user):
return
if not db:
await callback.answer("❌ База данных недоступна", show_alert=True)
return
try:
all_plans = await db.get_all_subscriptions_admin()
regular_plans = []
imported_plans = []
suspicious_plans = []
for plan in all_plans:
if getattr(plan, 'is_imported', False):
imported_plans.append(plan)
elif plan.is_trial:
continue # Пропускаем триальные
elif (plan.name.startswith(('Import_', 'Auto_', 'Imported_')) or
(plan.price == 0 and any(keyword in plan.name.lower() for keyword in
['импорт', 'default', 'squad', 'user_']))):
suspicious_plans.append(plan)
else:
regular_plans.append(plan)
text = f"📋 Анализ планов подписок\n\n"
text += f"🛒 Обычные планы (для покупки): {len(regular_plans)}\n"
if regular_plans:
for plan in regular_plans[:3]:
status = "🟢" if plan.is_active else "🔴"
text += f"{status} {plan.name} - {plan.price}\n"
if len(regular_plans) > 3:
text += f"... и еще {len(regular_plans) - 3}\n"
text += "\n"
text += f"📦 Импортированные планы: {len(imported_plans)}\n"
if imported_plans:
for plan in imported_plans[:3]:
status = "🟢" if plan.is_active else "🔴"
squad_short = plan.squad_uuid[:8] + "..." if plan.squad_uuid else "No Squad"
text += f"{status} {plan.name} ({squad_short})\n"
if len(imported_plans) > 3:
text += f"... и еще {len(imported_plans) - 3}\n"
text += "\n"
if suspicious_plans:
text += f"⚠️ Возможно импортированные: {len(suspicious_plans)}\n"
for plan in suspicious_plans[:3]:
status = "🟢" if plan.is_active else "🔴"
squad_short = plan.squad_uuid[:8] + "..." if plan.squad_uuid else "No Squad"
text += f"{status} {plan.name} ({squad_short})\n"
if len(suspicious_plans) > 3:
text += f"... и еще {len(suspicious_plans) - 3}\n"
text += "\n"
text += f"📊 Итого:\n"
text += f"Всего планов: {len(all_plans)}\n"
text += f"• Обычных: {len(regular_plans)}\n"
text += f"• Импортированных: {len(imported_plans)}\n"
if suspicious_plans:
text += f"• Нужно проверить: {len(suspicious_plans)}\n"
buttons = []
if suspicious_plans or any(plan.name != "Старая подписка" for plan in imported_plans):
buttons.append([InlineKeyboardButton(text="🏷 Переименовать импортированные", callback_data="rename_imported_plans")])
if imported_plans or suspicious_plans:
buttons.append([InlineKeyboardButton(text="🗑 Удалить импортированные", callback_data="delete_imported_plans")])
buttons.extend([
[InlineKeyboardButton(text="🔄 Обновить", callback_data="view_imported_plans")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="sync_remnawave")]
])
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
await callback.message.edit_text(text, reply_markup=keyboard)
except Exception as e:
logger.error(f"Error viewing imported plans: {e}")
await callback.answer("❌ Ошибка загрузки планов", show_alert=True)
@admin_router.callback_query(F.data == "delete_imported_plans")
async def delete_imported_plans_callback(callback: CallbackQuery, user: User, db: Database = None, **kwargs):
if not await check_admin_access(callback, user):
return
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да, удалить ВСЕ", callback_data="confirm_delete_imported"),
InlineKeyboardButton(text="❌ Отмена", callback_data="view_imported_plans")
]
])
await callback.message.edit_text(
"⚠️ ВНИМАНИЕ!\n\n"
"Вы уверены, что хотите удалить ВСЕ импортированные планы?\n\n"
"Это приведет к удалению:\n"
"Всех скрытых планов подписок\n"
"• Связанных пользовательских подписок\n\n"
"❗️ ДАННОЕ ДЕЙСТВИЕ НЕЛЬЗЯ ОТМЕНИТЬ!",
reply_markup=keyboard
)
@admin_router.callback_query(F.data == "confirm_delete_imported")
async def confirm_delete_imported_callback(callback: CallbackQuery, user: User, db: Database = None, **kwargs):
if not await check_admin_access(callback, user):
return
if not db:
await callback.answer("❌ База данных недоступна", show_alert=True)
return
try:
await callback.answer("🗑 Удаляю импортированные планы...")
progress_msg = await callback.message.edit_text("⏳ Удаление импортированных планов и связанных подписок...")
all_plans = await db.get_all_subscriptions_admin()
imported_plans = [plan for plan in all_plans if getattr(plan, 'is_imported', False)]
for plan in all_plans:
if (plan.name == "Старая подписка" and
plan not in imported_plans):
imported_plans.append(plan)
deleted_plans = 0
deleted_user_subscriptions = 0
errors = 0
for plan in imported_plans:
try:
user_subscriptions = await db.get_user_subscriptions_by_plan_id(plan.id)
for user_sub in user_subscriptions:
try:
success = await db.delete_user_subscription(user_sub.id)
if success:
deleted_user_subscriptions += 1
logger.info(f"Deleted user subscription {user_sub.id} (shortUuid: {user_sub.short_uuid})")
except Exception as e:
logger.error(f"Error deleting user subscription {user_sub.id}: {e}")
errors += 1
success = await db.delete_subscription(plan.id)
if success:
deleted_plans += 1
logger.info(f"Deleted imported plan: {plan.name} (ID: {plan.id})")
else:
errors += 1
except Exception as e:
logger.error(f"Error deleting imported plan {plan.id}: {e}")
errors += 1
result_text = (
f"✅ Удаление импортированных данных завершено!\n\n"
f"📊 Результаты:\n"
f"• Удалено планов: {deleted_plans}\n"
f"• Удалено пользовательских подписок: {deleted_user_subscriptions}\n"
f"• Ошибок: {errors}\n\n"
f"🔄 Теперь импорт можно запустить заново\n\n"
f"🕐 Завершено: {format_datetime(datetime.now(), user.language)}"
)
await progress_msg.edit_text(
result_text,
reply_markup=sync_remnawave_keyboard(user.language)
)
log_user_action(user.telegram_id, "deleted_imported_all", f"Plans: {deleted_plans}, UserSubs: {deleted_user_subscriptions}")
except Exception as e:
logger.error(f"Error deleting imported plans: {e}")
await callback.message.edit_text(
f"❌ Ошибка удаления планов\n\n{str(e)[:200]}",
reply_markup=sync_remnawave_keyboard(user.language)
)
@admin_router.callback_query(F.data == "debug_all_plans")
async def debug_all_plans_callback(callback: CallbackQuery, user: User, db: Database = None, **kwargs):
if not await check_admin_access(callback, user):
return
if not db:
await callback.answer("❌ База данных недоступна", show_alert=True)
return
try:
await callback.answer("🔍 Анализирую все планы...")
all_plans = await db.get_all_subscriptions_admin()
if not all_plans:
await callback.message.edit_text(
"❌ Планы не найдены в базе данных",
reply_markup=sync_remnawave_keyboard(user.language)
)
return
analysis = f"🔍 Анализ всех планов ({len(all_plans)} шт.)\n\n"
for i, plan in enumerate(all_plans, 1):
analysis += f"=== ПЛАН {i} ===\n"
analysis += f"ID: {plan.id}\n"
analysis += f"Название: {plan.name}\n"
analysis += f"Цена: {plan.price}\n"
analysis += f"Активен: {'Да' if plan.is_active else 'Нет'}\n"
analysis += f"Триальный: {'Да' if getattr(plan, 'is_trial', False) else 'Нет'}\n"
analysis += f"Импортированный: {'Да' if getattr(plan, 'is_imported', False) else 'Нет'}\n"
if plan.squad_uuid:
analysis += f"Squad UUID: {plan.squad_uuid[:20]}...\n"
else:
analysis += f"Squad UUID: НЕТ\n"
if plan.description:
desc_short = plan.description[:50] + "..." if len(plan.description) > 50 else plan.description
analysis += f"Описание: {desc_short}\n"
looks_imported = (
getattr(plan, 'is_imported', False) or
plan.name.startswith(('Import_', 'Auto_', 'Imported_', 'Trial_')) or
(plan.price == 0 and any(keyword in plan.name.lower() for keyword in
['импорт', 'default', 'squad', 'user_', 'trial']))
)
analysis += f"Создан: {plan.created_at.strftime('%Y-%m-%d %H:%M') if plan.created_at else 'N/A'}\n"
analysis += "\n"
max_length = 4000
if len(analysis) > max_length:
parts = []
current_part = ""
for line in analysis.split('\n'):
if len(current_part + line + '\n') > max_length:
if current_part:
parts.append(current_part.strip())
current_part = ""
current_part += line + '\n'
if current_part:
parts.append(current_part.strip())
for i, part in enumerate(parts):
if i == 0:
await callback.message.edit_text(part)
else:
await callback.message.answer(f"Часть {i+1}:\n\n{part}")
else:
await callback.message.edit_text(analysis)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🏷 Переименовать Trial_", callback_data="rename_imported_plans")],
[InlineKeyboardButton(text="📋 Просмотр планов", callback_data="view_imported_plans")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="sync_remnawave")]
])
await callback.message.answer(
"✅ Анализ завершен",
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Error debugging all plans: {e}")
await callback.answer("❌ Ошибка анализа планов", show_alert=True)
@admin_router.callback_query(F.data == "admin_referrals")
async def admin_referrals_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
await callback.message.edit_text(
"👥 Управление реферальной программой",
reply_markup=admin_referrals_keyboard(user.language)
)
@admin_router.callback_query(F.data == "referral_statistics")
async def referral_statistics_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
if not await check_admin_access(callback, user):
return
try:
async with db.session_factory() as session:
from sqlalchemy import select, func, and_
total_referrals = await session.execute(
select(func.count(ReferralProgram.id)).where(
and_(
ReferralProgram.referred_id < 900000000,
ReferralProgram.referred_id > 0
)
)
)
total_referrals = total_referrals.scalar() or 0
active_referrals = await session.execute(
select(func.count(ReferralProgram.id)).where(
and_(
ReferralProgram.first_reward_paid == True,
ReferralProgram.referred_id < 900000000,
ReferralProgram.referred_id > 0
)
)
)
active_referrals = active_referrals.scalar() or 0
total_paid = await session.execute(
select(func.sum(ReferralEarning.amount))
)
total_paid = total_paid.scalar() or 0.0
top_referrers = await session.execute(
select(
ReferralProgram.referrer_id,
func.count(ReferralProgram.id).label('count')
).where(
and_(
ReferralProgram.referred_id < 900000000,
ReferralProgram.referred_id > 0
)
).group_by(ReferralProgram.referrer_id)
.order_by(func.count(ReferralProgram.id).desc())
.limit(5)
)
top_referrers = list(top_referrers.fetchall())
text = "📊 Статистика реферальной программы\n\n"
text += f"👥 Всего рефералов: {total_referrals}\n"
text += f"✅ Активных рефералов: {active_referrals}\n"
text += f"💰 Выплачено всего: {total_paid:.2f}\n"
if total_referrals > 0:
conversion = (active_referrals / total_referrals * 100)
text += f"📈 Конверсия: {conversion:.1f}%\n"
else:
text += f"📈 Конверсия: 0%\n"
if top_referrers:
text += f"\n🏆 Топ рефереров:\n"
for i, (referrer_id, count) in enumerate(top_referrers, 1):
try:
referrer = await db.get_user_by_telegram_id(referrer_id)
if referrer:
display_name = ""
if referrer.first_name:
display_name = referrer.first_name
if referrer.username:
display_name += f" (@{referrer.username})" if display_name else f"@{referrer.username}"
if not display_name:
display_name = f"Пользователь {referrer_id}"
text += f"{i}. {display_name}: {count} рефералов\n"
else:
text += f"{i}. ID:{referrer_id}: {count} рефералов\n"
except Exception as e:
logger.error(f"Error getting referrer info for {referrer_id}: {e}")
text += f"{i}. ID:{referrer_id}: {count} рефералов\n"
try:
async with db.session_factory() as session:
first_rewards = await session.execute(
select(func.count(ReferralEarning.id), func.sum(ReferralEarning.amount))
.where(ReferralEarning.earning_type == 'first_reward')
)
first_rewards_data = first_rewards.fetchone()
percentage_rewards = await session.execute(
select(func.count(ReferralEarning.id), func.sum(ReferralEarning.amount))
.where(ReferralEarning.earning_type == 'percentage')
)
percentage_rewards_data = percentage_rewards.fetchone()
text += f"\n💸 Детализация выплат:\n"
if first_rewards_data and first_rewards_data[0]:
text += f"• Первые награды: {first_rewards_data[0]} шт. ({first_rewards_data[1]:.2f}₽)\n"
if percentage_rewards_data and percentage_rewards_data[0]:
text += f"• Процентные: {percentage_rewards_data[0]} шт. ({percentage_rewards_data[1]:.2f}₽)\n"
except Exception as e:
logger.error(f"Error getting payment stats: {e}")
await callback.message.edit_text(
text,
reply_markup=back_keyboard("admin_referrals", user.language)
)
except Exception as e:
logger.error(f"Error getting referral statistics: {e}")
await callback.message.edit_text(
"❌ Ошибка получения статистики",
reply_markup=back_keyboard("admin_referrals", user.language)
)
@admin_router.callback_query(F.data == "list_referrers")
async def list_referrers_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
if not await check_admin_access(callback, user):
return
try:
async with db.session_factory() as session:
from sqlalchemy import select, func, and_, case
top_referrers = await session.execute(
select(
ReferralProgram.referrer_id,
func.count(ReferralProgram.id).label('total_referrals'),
func.count(case((ReferralProgram.first_reward_paid == True, 1))).label('active_referrals'),
func.sum(ReferralProgram.total_earned).label('total_earned')
).where(
and_(
ReferralProgram.referred_id < 900000000,
ReferralProgram.referred_id > 0
)
).group_by(ReferralProgram.referrer_id)
.order_by(func.count(ReferralProgram.id).desc())
.limit(10)
)
referrers_data = list(top_referrers.fetchall())
if not referrers_data:
await callback.message.edit_text(
"📊 Список рефереров пуст\n\nПока никто не пригласил пользователей.",
reply_markup=back_keyboard("admin_referrals", user.language)
)
return
text = f"👥 Топ-{len(referrers_data)} рефереров:\n\n"
for i, (referrer_id, total_refs, active_refs, total_earned) in enumerate(referrers_data, 1):
try:
referrer = await db.get_user_by_telegram_id(referrer_id)
if referrer:
# Формируем имя
display_name = ""
if referrer.first_name:
display_name = referrer.first_name[:15]
if referrer.username:
username_part = f"@{referrer.username}"
if display_name:
display_name += f" ({username_part})"
else:
display_name = username_part
if not display_name:
display_name = f"Пользователь {referrer_id}"
else:
display_name = f"ID:{referrer_id}"
text += f"{i}. {display_name}\n"
text += f" 👥 Всего: {total_refs} | ✅ Активных: {active_refs or 0}\n"
text += f" 💰 Заработано: {total_earned or 0:.2f}\n\n"
except Exception as e:
logger.error(f"Error processing referrer {referrer_id}: {e}")
text += f"{i}. ID:{referrer_id}\n"
text += f" 👥 Всего: {total_refs} | ✅ Активных: {active_refs or 0}\n"
text += f" 💰 Заработано: {total_earned or 0:.2f}\n\n"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Обновить", callback_data="list_referrers")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_referrals")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
except Exception as e:
logger.error(f"Error listing referrers: {e}")
await callback.message.edit_text(
"❌ Ошибка получения списка рефереров",
reply_markup=back_keyboard("admin_referrals", user.language)
)
@admin_router.callback_query(F.data == "referral_payments")
async def referral_payments_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
if not await check_admin_access(callback, user):
return
try:
async with db.session_factory() as session:
from sqlalchemy import select, desc
recent_earnings = await session.execute(
select(ReferralEarning)
.order_by(desc(ReferralEarning.created_at))
.limit(15)
)
earnings = list(recent_earnings.scalars().all())
if not earnings:
await callback.message.edit_text(
"💰 История выплат пуста\n\nРеферальных выплат пока не было.",
reply_markup=back_keyboard("admin_referrals", user.language)
)
return
text = f"💰 Последние {len(earnings)} выплат:\n\n"
for earning in earnings:
try:
referrer = await db.get_user_by_telegram_id(earning.referrer_id)
referred = await db.get_user_by_telegram_id(earning.referred_id)
referrer_name = "Unknown"
if referrer:
if referrer.username:
referrer_name = f"@{referrer.username}"
elif referrer.first_name:
referrer_name = referrer.first_name[:10]
else:
referrer_name = f"ID:{earning.referrer_id}"
referred_name = "Unknown"
if referred:
if referred.username:
referred_name = f"@{referred.username}"
elif referred.first_name:
referred_name = referred.first_name[:10]
else:
referred_name = f"ID:{earning.referred_id}"
earning_type_emoji = "🎁" if earning.earning_type == "first_reward" else "💵"
earning_type_name = "Первая награда" if earning.earning_type == "first_reward" else "Процент"
date_str = earning.created_at.strftime("%d.%m %H:%M")
text += f"{earning_type_emoji} {earning.amount:.2f}₽ - {earning_type_name}\n"
text += f" От: {referrer_name}{referred_name}\n"
text += f" 📅 {date_str}\n\n"
except Exception as e:
logger.error(f"Error processing earning {earning.id}: {e}")
text += f"💰 {earning.amount:.2f}₽ - {earning.earning_type}\n"
text += f" ID: {earning.referrer_id}{earning.referred_id}\n\n"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Обновить", callback_data="referral_payments")],
[InlineKeyboardButton(text="📊 Статистика", callback_data="referral_statistics")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_referrals")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
except Exception as e:
logger.error(f"Error getting referral payments: {e}")
await callback.message.edit_text(
"❌ Ошибка получения истории выплат",
reply_markup=back_keyboard("admin_referrals", user.language)
)
@admin_router.callback_query(F.data == "referral_settings")
async def referral_settings_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
try:
import os
first_reward = float(os.getenv('REFERRAL_FIRST_REWARD', '150.0'))
referred_bonus = float(os.getenv('REFERRAL_REFERRED_BONUS', '150.0'))
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
percentage = float(os.getenv('REFERRAL_PERCENTAGE', '0.25'))
text = "⚙️ Настройки реферальной программы\n\n"
text += "📋 Текущие параметры:\n\n"
text += f"💰 Первая награда рефереру: {first_reward:.0f}\n"
text += f"🎁 Бонус приглашенному: {referred_bonus:.0f}\n"
text += f"💳 Порог активации: {threshold:.0f}\n"
text += f"📊 Процент с платежей: {percentage*100:.0f}%\n\n"
text += " Как это работает:\n"
text += f"1. Пользователь регистрируется по ссылке\n"
text += f"2. Пополняет баланс на {threshold:.0f}₽ или больше\n"
text += f"3. Реферер получает {first_reward:.0f}₽, новичок {referred_bonus:.0f}\n"
text += f"4. С каждого платежа новичка реферер получает {percentage*100:.0f}%\n\n"
text += "⚠️ Для изменения настроек отредактируйте .env файл:\n"
text += "• REFERRAL_FIRST_REWARD\n"
text += "• REFERRAL_REFERRED_BONUS\n"
text += "• REFERRAL_THRESHOLD\n"
text += "• REFERRAL_PERCENTAGE"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📊 Статистика", callback_data="referral_statistics")],
[InlineKeyboardButton(text="👥 Рефереры", callback_data="list_referrers")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_referrals")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
except Exception as e:
logger.error(f"Error showing referral settings: {e}")
await callback.message.edit_text(
"❌ Ошибка получения настроек",
reply_markup=back_keyboard("admin_referrals", user.language)
)