mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-03-04 04:43:21 +00:00
5642 lines
252 KiB
Python
5642 lines
252 KiB
Python
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
|
||
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:
|
||
# Fallback функции если api_error_handlers не найден
|
||
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):
|
||
"""Start subscription creation"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
t('enter_sub_name', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_create_sub_name)
|
||
|
||
@admin_router.message(StateFilter(BotStates.admin_create_sub_name))
|
||
async def handle_sub_name(message: Message, state: FSMContext, user: User, **kwargs):
|
||
"""Handle subscription name input"""
|
||
name = message.text.strip()
|
||
if len(name) < 3 or len(name) > 100:
|
||
await message.answer("❌ Название должно быть от 3 до 100 символов")
|
||
return
|
||
|
||
await state.update_data(name=name)
|
||
await message.answer(
|
||
t('enter_sub_description', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_create_sub_desc)
|
||
|
||
@admin_router.message(StateFilter(BotStates.admin_create_sub_desc))
|
||
async def handle_sub_description(message: Message, state: FSMContext, user: User, **kwargs):
|
||
"""Handle subscription description input"""
|
||
description = message.text.strip()
|
||
if len(description) > 500:
|
||
await message.answer("❌ Описание не должно превышать 500 символов")
|
||
return
|
||
|
||
await state.update_data(description=description)
|
||
await message.answer(
|
||
t('enter_sub_price', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_create_sub_price)
|
||
|
||
@admin_router.message(StateFilter(BotStates.admin_create_sub_price))
|
||
async def handle_sub_price(message: Message, state: FSMContext, user: User, **kwargs):
|
||
"""Handle subscription price input"""
|
||
is_valid, price = is_valid_amount(message.text)
|
||
|
||
if not is_valid:
|
||
await message.answer(t('invalid_amount', user.language))
|
||
return
|
||
|
||
await state.update_data(price=price)
|
||
await message.answer(
|
||
t('enter_sub_days', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_create_sub_days)
|
||
|
||
@admin_router.message(StateFilter(BotStates.admin_create_sub_days))
|
||
async def handle_sub_days(message: Message, state: FSMContext, user: User, **kwargs):
|
||
"""Handle subscription duration input"""
|
||
try:
|
||
days = int(message.text.strip())
|
||
if days <= 0 or days > 365:
|
||
await message.answer("❌ Длительность должна быть от 1 до 365 дней")
|
||
return
|
||
except ValueError:
|
||
await message.answer("❌ Введите число")
|
||
return
|
||
|
||
await state.update_data(days=days)
|
||
await message.answer(
|
||
t('enter_sub_traffic', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_create_sub_traffic)
|
||
|
||
@admin_router.message(StateFilter(BotStates.admin_create_sub_traffic))
|
||
async def handle_sub_traffic(message: Message, state: FSMContext, user: User, api: RemnaWaveAPI = None, **kwargs):
|
||
"""Handle subscription traffic limit input"""
|
||
try:
|
||
traffic_gb = int(message.text.strip())
|
||
if traffic_gb < 0 or traffic_gb > 10000:
|
||
await message.answer("❌ Лимит трафика должен быть от 0 до 10000 ГБ")
|
||
return
|
||
except ValueError:
|
||
await message.answer("❌ Введите число")
|
||
return
|
||
|
||
await state.update_data(traffic_gb=traffic_gb)
|
||
|
||
# Try to get squads from RemnaWave API
|
||
if api:
|
||
try:
|
||
logger.info("Attempting to fetch squads from API")
|
||
squads = await api.get_internal_squads_list()
|
||
logger.info(f"API returned squads: {squads}")
|
||
|
||
if squads and len(squads) > 0:
|
||
logger.info(f"Found {len(squads)} squads, showing selection keyboard")
|
||
await message.answer(
|
||
"📋 Выберите Squad из списка:",
|
||
reply_markup=squad_selection_keyboard(squads, user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_create_sub_squad_select)
|
||
return
|
||
else:
|
||
logger.warning("No squads returned from API or empty list")
|
||
except Exception as e:
|
||
logger.error(f"Failed to get squads from API: {e}", exc_info=True)
|
||
else:
|
||
logger.warning("No API instance provided")
|
||
|
||
# Fallback to manual input if API fails
|
||
logger.info("Falling back to manual squad UUID input")
|
||
await message.answer(
|
||
t('enter_squad_uuid', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_create_sub_squad)
|
||
|
||
def squad_selection_keyboard(squads: List[Dict], language: str = 'ru') -> InlineKeyboardMarkup:
|
||
"""Create keyboard for squad selection"""
|
||
logger.info(f"Creating squad selection keyboard for {len(squads)} squads")
|
||
buttons = []
|
||
|
||
for squad in squads:
|
||
logger.debug(f"Processing squad: {squad}")
|
||
|
||
# Получаем название и UUID squad'а с проверкой
|
||
squad_name = squad.get('name', 'Unknown Squad')
|
||
squad_uuid = squad.get('uuid', '')
|
||
|
||
if not squad_uuid:
|
||
logger.warning(f"Squad without UUID: {squad}")
|
||
continue
|
||
|
||
# Truncate name if too long
|
||
if len(squad_name) > 30:
|
||
display_name = squad_name[:27] + "..."
|
||
else:
|
||
display_name = squad_name
|
||
|
||
# Добавляем информацию о количестве участников если есть
|
||
info_text = ""
|
||
if 'info' in squad:
|
||
members_count = squad['info'].get('membersCount', 0)
|
||
inbounds_count = squad['info'].get('inboundsCount', 0)
|
||
info_text = f" ({members_count}👥, {inbounds_count}🔗)"
|
||
|
||
button_text = f"📋 {display_name}{info_text}"
|
||
logger.debug(f"Creating button: {button_text} -> {squad_uuid}")
|
||
|
||
buttons.append([
|
||
InlineKeyboardButton(
|
||
text=button_text,
|
||
callback_data=f"select_squad_{squad_uuid}"
|
||
)
|
||
])
|
||
|
||
if not buttons:
|
||
logger.warning("No valid squads found for keyboard")
|
||
# Добавляем кнопку ручного ввода как единственную опцию
|
||
buttons.append([
|
||
InlineKeyboardButton(
|
||
text="✏️ Ввести UUID вручную",
|
||
callback_data="manual_squad_input"
|
||
)
|
||
])
|
||
else:
|
||
# Add manual input button as alternative
|
||
buttons.append([
|
||
InlineKeyboardButton(
|
||
text="✏️ Ввести UUID вручную",
|
||
callback_data="manual_squad_input"
|
||
)
|
||
])
|
||
|
||
# Add cancel button
|
||
buttons.append([
|
||
InlineKeyboardButton(
|
||
text=t('cancel', language),
|
||
callback_data="main_menu"
|
||
)
|
||
])
|
||
|
||
logger.info(f"Created keyboard with {len(buttons)} buttons")
|
||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
@admin_router.callback_query(F.data == "manual_squad_input")
|
||
async def manual_squad_input(callback: CallbackQuery, state: FSMContext, user: User, **kwargs):
|
||
"""Switch to manual squad UUID input"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
t('enter_squad_uuid', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_create_sub_squad)
|
||
|
||
@admin_router.callback_query(F.data.startswith("select_squad_"))
|
||
async def handle_squad_selection(callback: CallbackQuery, state: FSMContext, user: User, db: Database, **kwargs):
|
||
"""Handle squad selection from inline keyboard"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
try:
|
||
squad_uuid = callback.data.replace("select_squad_", "")
|
||
|
||
# Validate UUID format
|
||
if not validate_squad_uuid(squad_uuid):
|
||
await callback.answer("❌ Неверный формат UUID")
|
||
return
|
||
|
||
# Get all state data
|
||
data = await state.get_data()
|
||
|
||
# Create subscription in database
|
||
subscription = await db.create_subscription(
|
||
name=data['name'],
|
||
description=data['description'],
|
||
price=data['price'],
|
||
duration_days=data['days'],
|
||
traffic_limit_gb=data['traffic_gb'],
|
||
squad_uuid=squad_uuid
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
t('subscription_created', user.language),
|
||
reply_markup=admin_menu_keyboard(user.language)
|
||
)
|
||
|
||
log_user_action(user.telegram_id, "subscription_created", data['name'])
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error creating subscription: {e}")
|
||
await callback.message.edit_text(
|
||
t('error_occurred', user.language),
|
||
reply_markup=admin_menu_keyboard(user.language)
|
||
)
|
||
|
||
await state.clear()
|
||
|
||
@admin_router.message(StateFilter(BotStates.admin_create_sub_squad))
|
||
async def handle_sub_squad(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
|
||
"""Handle subscription squad UUID manual input (fallback)"""
|
||
squad_uuid = message.text.strip()
|
||
|
||
if not validate_squad_uuid(squad_uuid):
|
||
await message.answer("❌ Неверный формат UUID")
|
||
return
|
||
|
||
# Get all state data
|
||
data = await state.get_data()
|
||
|
||
try:
|
||
# Create subscription in database
|
||
subscription = await db.create_subscription(
|
||
name=data['name'],
|
||
description=data['description'],
|
||
price=data['price'],
|
||
duration_days=data['days'],
|
||
traffic_limit_gb=data['traffic_gb'],
|
||
squad_uuid=squad_uuid
|
||
)
|
||
|
||
await message.answer(
|
||
t('subscription_created', user.language),
|
||
reply_markup=admin_menu_keyboard(user.language)
|
||
)
|
||
|
||
log_user_action(user.telegram_id, "subscription_created", data['name'])
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error creating subscription: {e}")
|
||
await message.answer(
|
||
t('error_occurred', user.language),
|
||
reply_markup=admin_menu_keyboard(user.language)
|
||
)
|
||
|
||
await state.clear()
|
||
|
||
@admin_router.callback_query(F.data == "list_admin_subscriptions")
|
||
async def list_admin_subscriptions(callback: CallbackQuery, user: User, db: Database, **kwargs):
|
||
"""List all subscriptions for admin"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
try:
|
||
subs = await db.get_all_subscriptions(include_inactive=True, exclude_trial=True)
|
||
if not subs:
|
||
await callback.message.edit_text(
|
||
"❌ Подписки не найдены",
|
||
reply_markup=back_keyboard("admin_subscriptions", user.language)
|
||
)
|
||
return
|
||
|
||
keyboard = admin_subscriptions_list_keyboard(subs, user.language)
|
||
await callback.message.edit_text(
|
||
t('subscriptions_list', user.language),
|
||
reply_markup=keyboard
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Error listing subscriptions: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
@admin_router.callback_query(F.data.startswith("toggle_sub_"))
|
||
async def toggle_subscription(callback: CallbackQuery, user: User, db: Database, **kwargs):
|
||
"""Toggle subscription active status"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
try:
|
||
sub_id = int(callback.data.split("_")[2])
|
||
sub = await db.get_subscription_by_id(sub_id)
|
||
if not sub:
|
||
await callback.answer("❌ Подписка не найдена")
|
||
return
|
||
|
||
sub.is_active = not sub.is_active
|
||
await db.update_subscription(sub)
|
||
|
||
status = t('enabled', user.language) if sub.is_active else t('disabled', user.language)
|
||
await callback.answer(f"✅ Подписка «{sub.name}» {status}")
|
||
|
||
# Update the list
|
||
subs = await db.get_all_subscriptions(include_inactive=True)
|
||
await callback.message.edit_reply_markup(
|
||
reply_markup=admin_subscriptions_list_keyboard(subs, user.language)
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Error toggling subscription: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
@admin_router.callback_query(F.data.startswith("edit_sub_"))
|
||
async def edit_sub_menu(callback: CallbackQuery, state: FSMContext, user: User, **kwargs):
|
||
"""Show subscription edit menu"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
try:
|
||
sub_id = int(callback.data.split("_")[2])
|
||
await state.update_data(edit_sub_id=sub_id)
|
||
|
||
buttons = [
|
||
[InlineKeyboardButton(text="📝 Название", callback_data="edit_field_name")],
|
||
[InlineKeyboardButton(text="💰 Цена", callback_data="edit_field_price")],
|
||
[InlineKeyboardButton(text="📅 Дни", callback_data="edit_field_days")],
|
||
[InlineKeyboardButton(text="📊 Трафик", callback_data="edit_field_traffic")],
|
||
[InlineKeyboardButton(text="📋 Описание", callback_data="edit_field_description")],
|
||
[InlineKeyboardButton(text=t('back', user.language), callback_data="list_admin_subscriptions")]
|
||
]
|
||
kb = InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
await callback.message.edit_text("🔧 Выберите поле для редактирования:", reply_markup=kb)
|
||
except Exception as e:
|
||
logger.error(f"Error showing edit menu: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
@admin_router.callback_query(F.data.startswith("edit_field_"))
|
||
async def ask_new_value(callback: CallbackQuery, state: FSMContext, user: User, **kwargs):
|
||
"""Ask for new field value"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
field = callback.data.split("_")[2]
|
||
await state.update_data(edit_field=field)
|
||
|
||
field_names = {
|
||
'name': 'название',
|
||
'price': 'цену',
|
||
'days': 'количество дней',
|
||
'traffic': 'лимит трафика (ГБ)',
|
||
'description': 'описание'
|
||
}
|
||
|
||
field_name = field_names.get(field, field)
|
||
await callback.message.edit_text(
|
||
f"📝 Введите новое значение для поля '{field_name}':",
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_edit_sub_value)
|
||
@admin_router.message(StateFilter(BotStates.admin_edit_sub_value))
|
||
async def handle_edit_value(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
|
||
"""Handle new value for subscription field"""
|
||
data = await state.get_data()
|
||
sub_id = data.get('edit_sub_id')
|
||
field = data.get('edit_field')
|
||
new_value = message.text.strip()
|
||
|
||
try:
|
||
sub = await db.get_subscription_by_id(sub_id)
|
||
if not sub:
|
||
await message.answer("❌ Подписка не найдена")
|
||
await state.clear()
|
||
return
|
||
|
||
# Validate and set new value
|
||
if field == 'name':
|
||
if len(new_value) < 3 or len(new_value) > 100:
|
||
await message.answer("❌ Название должно быть от 3 до 100 символов")
|
||
return
|
||
sub.name = new_value
|
||
elif field == 'price':
|
||
is_valid, price = is_valid_amount(new_value)
|
||
if not is_valid:
|
||
await message.answer(t('invalid_amount', user.language))
|
||
return
|
||
sub.price = price
|
||
elif field == 'days':
|
||
try:
|
||
days = int(new_value)
|
||
if days <= 0 or days > 365:
|
||
await message.answer("❌ Длительность должна быть от 1 до 365 дней")
|
||
return
|
||
sub.duration_days = days
|
||
except ValueError:
|
||
await message.answer("❌ Введите число")
|
||
return
|
||
elif field == 'traffic':
|
||
try:
|
||
traffic = int(new_value)
|
||
if traffic < 0 or traffic > 10000:
|
||
await message.answer("❌ Лимит трафика должен быть от 0 до 10000 ГБ")
|
||
return
|
||
sub.traffic_limit_gb = traffic
|
||
except ValueError:
|
||
await message.answer("❌ Введите число")
|
||
return
|
||
elif field == 'description':
|
||
if len(new_value) > 500:
|
||
await message.answer("❌ Описание не должно превышать 500 символов")
|
||
return
|
||
sub.description = new_value
|
||
|
||
await db.update_subscription(sub)
|
||
await message.answer(
|
||
"✅ Подписка обновлена",
|
||
reply_markup=admin_menu_keyboard(user.language)
|
||
)
|
||
|
||
log_user_action(user.telegram_id, "subscription_edited", f"Sub: {sub.name}, Field: {field}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error updating subscription: {e}")
|
||
await message.answer(t('error_occurred', user.language))
|
||
|
||
await state.clear()
|
||
|
||
@admin_router.callback_query(F.data.startswith("delete_sub_"))
|
||
async def delete_subscription_confirm(callback: CallbackQuery, user: User, **kwargs):
|
||
"""Show subscription deletion confirmation"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
sub_id = int(callback.data.split("_")[2])
|
||
|
||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(text="✅ Да, удалить", callback_data=f"confirm_delete_sub_{sub_id}"),
|
||
InlineKeyboardButton(text="❌ Отмена", callback_data="list_admin_subscriptions")
|
||
]
|
||
])
|
||
|
||
await callback.message.edit_text(
|
||
"⚠️ Вы уверены, что хотите удалить эту подписку?\nЭто действие нельзя отменить!",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
@admin_router.callback_query(F.data.startswith("confirm_delete_sub_"))
|
||
async def delete_subscription(callback: CallbackQuery, user: User, db: Database, **kwargs):
|
||
"""Delete subscription"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
try:
|
||
sub_id = int(callback.data.split("_")[3])
|
||
sub = await db.get_subscription_by_id(sub_id)
|
||
|
||
if not sub:
|
||
await callback.answer("❌ Подписка не найдена")
|
||
return
|
||
|
||
success = await db.delete_subscription(sub_id)
|
||
|
||
if success:
|
||
await callback.answer(f"✅ Подписка «{sub.name}» удалена")
|
||
log_user_action(user.telegram_id, "subscription_deleted", sub.name)
|
||
else:
|
||
await callback.answer("❌ Ошибка удаления")
|
||
|
||
# Return to list
|
||
subs = await db.get_all_subscriptions(include_inactive=True)
|
||
if subs:
|
||
await callback.message.edit_text(
|
||
t('subscriptions_list', user.language),
|
||
reply_markup=admin_subscriptions_list_keyboard(subs, user.language)
|
||
)
|
||
else:
|
||
await callback.message.edit_text(
|
||
"❌ Подписки не найдены",
|
||
reply_markup=back_keyboard("admin_subscriptions", user.language)
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Error deleting subscription: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
# User management
|
||
@admin_router.callback_query(F.data == "admin_users")
|
||
async def admin_users_callback(callback: CallbackQuery, user: User, **kwargs):
|
||
"""Show user management"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
t('manage_users', user.language),
|
||
reply_markup=admin_users_keyboard(user.language)
|
||
)
|
||
|
||
@admin_router.callback_query(F.data == "list_users")
|
||
async def list_users_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
|
||
"""List all users"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
try:
|
||
users = await db.get_all_users()
|
||
|
||
if not users:
|
||
await callback.message.edit_text(
|
||
"❌ Пользователи не найдены",
|
||
reply_markup=back_keyboard("admin_users", user.language)
|
||
)
|
||
return
|
||
|
||
text = t('user_list', user.language) + "\n\n"
|
||
|
||
# Show first 20 users
|
||
for u in users[:20]:
|
||
username = u.username or "N/A"
|
||
text += t('user_item', user.language,
|
||
id=u.telegram_id,
|
||
username=username,
|
||
balance=u.balance
|
||
) + "\n"
|
||
|
||
if len(users) > 20:
|
||
text += f"\n... и еще {len(users) - 20} пользователей"
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=back_keyboard("admin_users", user.language)
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error listing users: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
# Balance management
|
||
@admin_router.callback_query(F.data == "admin_balance")
|
||
async def admin_balance_callback(callback: CallbackQuery, user: User, **kwargs):
|
||
"""Show balance management"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
t('manage_balance', user.language),
|
||
reply_markup=admin_balance_keyboard(user.language)
|
||
)
|
||
|
||
@admin_router.callback_query(F.data == "admin_add_balance")
|
||
async def admin_add_balance_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
|
||
"""Start adding balance to user"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
t('enter_user_id', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_add_balance_user)
|
||
|
||
@admin_router.message(StateFilter(BotStates.admin_add_balance_user))
|
||
async def handle_balance_user_id(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
|
||
"""Handle user ID input for balance addition"""
|
||
telegram_id = parse_telegram_id(message.text)
|
||
|
||
if not telegram_id:
|
||
await message.answer("❌ Неверный Telegram ID")
|
||
return
|
||
|
||
# Check if user exists
|
||
target_user = await db.get_user_by_telegram_id(telegram_id)
|
||
if not target_user:
|
||
await message.answer(t('user_not_found', user.language))
|
||
return
|
||
|
||
await state.update_data(target_user_id=telegram_id)
|
||
await message.answer(
|
||
t('enter_balance_amount', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_add_balance_amount)
|
||
|
||
@admin_router.message(StateFilter(BotStates.admin_add_balance_amount))
|
||
async def handle_balance_amount(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
|
||
"""Handle balance amount input"""
|
||
is_valid, amount = is_valid_amount(message.text)
|
||
|
||
if not is_valid:
|
||
await message.answer(t('invalid_amount', user.language))
|
||
return
|
||
|
||
data = await state.get_data()
|
||
target_user_id = data['target_user_id']
|
||
|
||
try:
|
||
# Add balance
|
||
success = await db.add_balance(target_user_id, amount)
|
||
|
||
if success:
|
||
# Create payment record
|
||
await db.create_payment(
|
||
user_id=target_user_id,
|
||
amount=amount,
|
||
payment_type='admin_topup',
|
||
description=f'Пополнение администратором (ID: {user.telegram_id})',
|
||
status='completed'
|
||
)
|
||
|
||
await message.answer(
|
||
t('balance_added', user.language),
|
||
reply_markup=admin_menu_keyboard(user.language)
|
||
)
|
||
|
||
log_user_action(user.telegram_id, "admin_balance_added", f"User: {target_user_id}, Amount: {amount}")
|
||
else:
|
||
await message.answer(t('user_not_found', user.language))
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error adding balance: {e}")
|
||
await message.answer(t('error_occurred', user.language))
|
||
|
||
await state.clear()
|
||
|
||
@admin_router.callback_query(F.data == "admin_payment_history")
|
||
async def admin_payment_history_callback(callback: CallbackQuery, user: User, db: Database, state: FSMContext, **kwargs):
|
||
"""Show payment history (first page) - ADMIN VERSION"""
|
||
logger.info(f"admin_payment_history_callback called for user {user.telegram_id}")
|
||
|
||
if not await check_admin_access(callback, user):
|
||
logger.warning(f"Admin access denied for user {user.telegram_id}")
|
||
return
|
||
|
||
logger.info("Admin access granted, clearing state and showing payment history")
|
||
await state.clear() # Очищаем состояние для новой пагинации
|
||
await show_payment_history_page(callback, user, db, state, page=0)
|
||
|
||
async def show_payment_history_page(callback: CallbackQuery, user: User, db: Database, state: FSMContext, page: int = 0):
|
||
"""Show payment history page with pagination"""
|
||
logger.info(f"show_payment_history_page called: page={page}, user={user.telegram_id}")
|
||
|
||
try:
|
||
page_size = 10
|
||
offset = page * page_size
|
||
|
||
# Получаем все платежи с пагинацией
|
||
payments, total_count = await db.get_all_payments_paginated(offset=offset, limit=page_size)
|
||
|
||
logger.info(f"Got {len(payments) if payments else 0} payments, total_count={total_count}")
|
||
|
||
if not payments and page == 0:
|
||
await callback.message.edit_text(
|
||
"❌ История платежей пуста",
|
||
reply_markup=back_keyboard("admin_balance", user.language)
|
||
)
|
||
return
|
||
|
||
# Если страница пустая, но не первая - возвращаемся на предыдущую
|
||
if not payments and page > 0:
|
||
await show_payment_history_page(callback, user, db, state, page - 1)
|
||
return
|
||
|
||
# Формируем текст
|
||
total_pages = (total_count + page_size - 1) // page_size
|
||
text = f"💳 История платежей (стр. {page + 1}/{total_pages})\n"
|
||
text += f"📊 Всего записей: {total_count}\n\n"
|
||
|
||
for payment in payments:
|
||
# Получаем информацию о пользователе
|
||
payment_user = await db.get_user_by_telegram_id(payment.user_id)
|
||
username = payment_user.username if payment_user and payment_user.username else "N/A"
|
||
first_name = payment_user.first_name if payment_user and payment_user.first_name else "N/A"
|
||
|
||
# Форматируем статус
|
||
status_emoji = {
|
||
'completed': '✅',
|
||
'pending': '⏳',
|
||
'cancelled': '❌'
|
||
}.get(payment.status, '❓')
|
||
|
||
# Форматируем тип платежа
|
||
type_emoji = {
|
||
'topup': '💰',
|
||
'subscription': '📱',
|
||
'subscription_extend': '🔄',
|
||
'promocode': '🎫',
|
||
'trial': '🆓',
|
||
'admin_topup': '👨💼'
|
||
}.get(payment.payment_type, '💳')
|
||
|
||
date_str = format_datetime(payment.created_at, user.language)
|
||
amount_str = f"+{payment.amount}" if payment.amount > 0 else str(payment.amount)
|
||
|
||
text += f"{status_emoji} {type_emoji} {amount_str} руб.\n"
|
||
text += f"👤 {first_name} (@{username}) ID:{payment.user_id}\n"
|
||
text += f"📝 {payment.description}\n"
|
||
text += f"📅 {date_str}\n\n"
|
||
|
||
# Сохраняем текущую страницу в состояние
|
||
await state.update_data(current_page=page)
|
||
await state.set_state(BotStates.admin_payment_history_page)
|
||
|
||
# Создаем клавиатуру с пагинацией
|
||
keyboard = create_pagination_keyboard(page, total_pages, "payment_history", user.language)
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error showing payment history: {e}")
|
||
await callback.message.edit_text(
|
||
t('error_occurred', user.language),
|
||
reply_markup=back_keyboard("admin_balance", user.language)
|
||
)
|
||
|
||
def create_pagination_keyboard(current_page: int, total_pages: int, callback_prefix: str, language: str) -> InlineKeyboardMarkup:
|
||
"""Create pagination keyboard"""
|
||
buttons = []
|
||
|
||
# Кнопки навигации
|
||
nav_buttons = []
|
||
|
||
if current_page > 0:
|
||
nav_buttons.append(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"{callback_prefix}_page_{current_page - 1}"))
|
||
|
||
if current_page < total_pages - 1:
|
||
nav_buttons.append(InlineKeyboardButton(text="Вперед ➡️", callback_data=f"{callback_prefix}_page_{current_page + 1}"))
|
||
|
||
if nav_buttons:
|
||
buttons.append(nav_buttons)
|
||
|
||
# Индикатор страницы
|
||
if total_pages > 1:
|
||
buttons.append([InlineKeyboardButton(text=f"📄 {current_page + 1}/{total_pages}", callback_data="noop")])
|
||
|
||
# Кнопка "Назад"
|
||
buttons.append([InlineKeyboardButton(text=t('back', language), callback_data="admin_balance")])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
@admin_router.callback_query(F.data.startswith("payment_history_page_"))
|
||
async def payment_history_page_callback(callback: CallbackQuery, user: User, db: Database, state: FSMContext, **kwargs):
|
||
"""Handle payment history pagination"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
try:
|
||
page = int(callback.data.split("_")[-1])
|
||
await show_payment_history_page(callback, user, db, state, page)
|
||
except (ValueError, IndexError) as e:
|
||
logger.error(f"Error parsing page number: {e}")
|
||
await callback.answer("❌ Ошибка навигации")
|
||
|
||
@admin_router.callback_query(F.data == "noop")
|
||
async def noop_callback(callback: CallbackQuery, **kwargs):
|
||
"""Handle no-operation callback (for page indicator)"""
|
||
await callback.answer()
|
||
|
||
# Payment approval handlers
|
||
@admin_router.callback_query(F.data.startswith("approve_payment_"))
|
||
async def approve_payment(callback: CallbackQuery, user: User, db: Database, **kwargs):
|
||
"""Approve payment"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
try:
|
||
payment_id = int(callback.data.split("_")[2])
|
||
payment = await db.get_payment_by_id(payment_id)
|
||
|
||
if not payment:
|
||
await callback.answer("❌ Платеж не найден")
|
||
return
|
||
|
||
if payment.status != 'pending':
|
||
await callback.answer("❌ Платеж уже обработан")
|
||
return
|
||
|
||
# Add balance to user
|
||
success = await db.add_balance(payment.user_id, payment.amount)
|
||
|
||
if success:
|
||
# Update payment status
|
||
payment.status = 'completed'
|
||
await db.update_payment(payment)
|
||
|
||
await callback.message.edit_text(
|
||
f"✅ Платеж одобрен!\n💰 Пользователю {payment.user_id} добавлено {payment.amount} руб."
|
||
)
|
||
|
||
# Notify user about successful payment
|
||
bot = kwargs.get('bot')
|
||
if bot:
|
||
try:
|
||
await bot.send_message(
|
||
payment.user_id,
|
||
f"✅ Ваш баланс пополнен на {payment.amount} руб."
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Failed to notify user {payment.user_id}: {e}")
|
||
|
||
log_user_action(user.telegram_id, "payment_approved", f"Payment: {payment_id}, Amount: {payment.amount}")
|
||
else:
|
||
await callback.answer("❌ Ошибка при пополнении баланса")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error approving payment: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
@admin_router.callback_query(F.data.startswith("reject_payment_"))
|
||
async def reject_payment(callback: CallbackQuery, user: User, db: Database, **kwargs):
|
||
"""Reject payment"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
try:
|
||
payment_id = int(callback.data.split("_")[2])
|
||
payment = await db.get_payment_by_id(payment_id)
|
||
|
||
if not payment:
|
||
await callback.answer("❌ Платеж не найден")
|
||
return
|
||
|
||
if payment.status != 'pending':
|
||
await callback.answer("❌ Платеж уже обработан")
|
||
return
|
||
|
||
# Update payment status
|
||
payment.status = 'cancelled'
|
||
await db.update_payment(payment)
|
||
|
||
await callback.message.edit_text(
|
||
f"❌ Платеж отклонен!\n💰 Платеж пользователя {payment.user_id} на сумму {payment.amount} руб. отклонен."
|
||
)
|
||
|
||
# Notify user about rejected payment
|
||
bot = kwargs.get('bot')
|
||
if bot:
|
||
try:
|
||
await bot.send_message(
|
||
payment.user_id,
|
||
f"❌ Ваш запрос на пополнение баланса на {payment.amount} руб. отклонен."
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Failed to notify user {payment.user_id}: {e}")
|
||
|
||
log_user_action(user.telegram_id, "payment_rejected", f"Payment: {payment_id}, Amount: {payment.amount}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error rejecting payment: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
# Promocode management
|
||
@admin_router.callback_query(F.data == "admin_promocodes")
|
||
async def admin_promocodes_callback(callback: CallbackQuery, user: User, **kwargs):
|
||
"""Show promocode management"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
t('manage_promocodes', user.language),
|
||
reply_markup=admin_promocodes_keyboard(user.language)
|
||
)
|
||
|
||
@admin_router.callback_query(F.data == "create_promocode")
|
||
async def create_promocode_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
|
||
"""Start promocode creation"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
t('enter_promo_code', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_create_promo_code)
|
||
|
||
@admin_router.message(StateFilter(BotStates.admin_create_promo_code))
|
||
async def handle_promo_code(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
|
||
"""Handle promocode input"""
|
||
code = message.text.strip().upper()
|
||
|
||
if not validate_promocode_format(code):
|
||
await message.answer("❌ Промокод должен содержать только буквы и цифры (3-20 символов)")
|
||
return
|
||
|
||
# Check if promocode already exists
|
||
existing = await db.get_promocode_by_code(code)
|
||
if existing:
|
||
await message.answer(t('promocode_exists', user.language))
|
||
return
|
||
|
||
await state.update_data(code=code)
|
||
await message.answer(
|
||
t('enter_promo_discount', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_create_promo_discount)
|
||
|
||
@admin_router.message(StateFilter(BotStates.admin_create_promo_discount))
|
||
async def handle_promo_discount(message: Message, state: FSMContext, user: User, **kwargs):
|
||
"""Handle promocode discount input"""
|
||
is_valid, discount = is_valid_amount(message.text)
|
||
|
||
if not is_valid:
|
||
await message.answer(t('invalid_amount', user.language))
|
||
return
|
||
|
||
await state.update_data(discount=discount)
|
||
await message.answer(
|
||
t('enter_promo_limit', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_create_promo_limit)
|
||
|
||
@admin_router.message(StateFilter(BotStates.admin_create_promo_limit))
|
||
async def handle_promo_limit(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
|
||
"""Handle promocode usage limit input"""
|
||
try:
|
||
limit = int(message.text.strip())
|
||
if limit <= 0 or limit > 10000:
|
||
await message.answer("❌ Лимит должен быть от 1 до 10000")
|
||
return
|
||
except ValueError:
|
||
await message.answer("❌ Введите число")
|
||
return
|
||
|
||
data = await state.get_data()
|
||
|
||
try:
|
||
# Create promocode
|
||
promocode = await db.create_promocode(
|
||
code=data['code'],
|
||
discount_amount=data['discount'],
|
||
usage_limit=limit
|
||
)
|
||
|
||
await message.answer(
|
||
t('promocode_created', user.language),
|
||
reply_markup=admin_menu_keyboard(user.language)
|
||
)
|
||
|
||
log_user_action(user.telegram_id, "promocode_created", data['code'])
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error creating promocode: {e}")
|
||
await message.answer(
|
||
t('error_occurred', user.language),
|
||
reply_markup=admin_menu_keyboard(user.language)
|
||
)
|
||
|
||
await state.clear()
|
||
|
||
@admin_router.callback_query(F.data == "list_promocodes")
|
||
async def list_promocodes_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
|
||
"""List all promocodes"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
try:
|
||
promocodes = await db.get_all_promocodes()
|
||
|
||
if not promocodes:
|
||
await callback.message.edit_text(
|
||
"❌ Промокоды не найдены",
|
||
reply_markup=back_keyboard("admin_promocodes", user.language)
|
||
)
|
||
return
|
||
|
||
text = "📋 Список промокодов:\n\n"
|
||
|
||
for promo in promocodes[:10]: # Show first 10
|
||
status = "🟢" if promo.is_active else "🔴"
|
||
expiry = ""
|
||
if promo.expires_at:
|
||
expiry = f" (до {format_date(promo.expires_at, user.language)})"
|
||
|
||
text += f"{status} `{promo.code}` - {promo.discount_amount}р.\n"
|
||
text += f" Использовано: {promo.used_count}/{promo.usage_limit}{expiry}\n\n"
|
||
|
||
if len(promocodes) > 10:
|
||
text += f"... и еще {len(promocodes) - 10} промокодов"
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=back_keyboard("admin_promocodes", user.language),
|
||
parse_mode='Markdown'
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error listing promocodes: {e}")
|
||
await callback.answer(t('error_occurred', user.language))
|
||
|
||
@admin_router.callback_query(F.data == "main_menu", StateFilter(
|
||
BotStates.admin_create_sub_name,
|
||
BotStates.admin_create_sub_desc,
|
||
BotStates.admin_create_sub_price,
|
||
BotStates.admin_create_sub_days,
|
||
BotStates.admin_create_sub_traffic,
|
||
BotStates.admin_create_sub_squad,
|
||
BotStates.admin_add_balance_user,
|
||
BotStates.admin_add_balance_amount,
|
||
BotStates.admin_create_promo_code,
|
||
BotStates.admin_create_promo_discount,
|
||
BotStates.admin_create_promo_limit,
|
||
BotStates.admin_edit_sub_value,
|
||
BotStates.admin_send_message_user,
|
||
BotStates.admin_send_message_text,
|
||
BotStates.admin_broadcast_text,
|
||
BotStates.admin_payment_history_page,
|
||
BotStates.admin_search_user_any, # Добавляем новый state
|
||
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):
|
||
"""Cancel admin action and return to main menu"""
|
||
await state.clear()
|
||
await callback.message.edit_text(
|
||
t('main_menu', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
|
||
@admin_router.callback_query(F.data == "admin_messages")
|
||
async def admin_messages_callback(callback: CallbackQuery, user: User, **kwargs):
|
||
"""Show message management"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
t('send_message', user.language),
|
||
reply_markup=admin_messages_keyboard(user.language)
|
||
)
|
||
|
||
@admin_router.callback_query(F.data == "admin_send_to_user")
|
||
async def admin_send_to_user_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
|
||
"""Start sending message to specific user"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
t('enter_user_id_message', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_send_message_user)
|
||
|
||
@admin_router.message(StateFilter(BotStates.admin_send_message_user))
|
||
async def handle_message_user_id(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
|
||
"""Handle user ID input for message sending"""
|
||
telegram_id = parse_telegram_id(message.text)
|
||
|
||
if not telegram_id:
|
||
await message.answer("❌ Неверный Telegram ID")
|
||
return
|
||
|
||
# Check if user exists
|
||
target_user = await db.get_user_by_telegram_id(telegram_id)
|
||
if not target_user:
|
||
await message.answer(t('user_not_found', user.language))
|
||
return
|
||
|
||
await state.update_data(target_user_id=telegram_id)
|
||
await message.answer(
|
||
t('enter_message_text', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_send_message_text)
|
||
|
||
# Monitor service management
|
||
@admin_router.callback_query(F.data == "admin_monitor")
|
||
async def admin_monitor_callback(callback: CallbackQuery, user: User, **kwargs):
|
||
"""Show monitor service management"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
"🔍 Управление сервисом мониторинга",
|
||
reply_markup=admin_monitor_keyboard(user.language)
|
||
)
|
||
|
||
@admin_router.callback_query(F.data == "monitor_status")
|
||
async def monitor_status_callback(callback: CallbackQuery, user: User, **kwargs):
|
||
"""Show monitor service status"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
monitor_service = kwargs.get('monitor_service')
|
||
if not monitor_service:
|
||
await callback.message.edit_text(
|
||
"❌ Сервис мониторинга недоступен",
|
||
reply_markup=back_keyboard("admin_monitor", user.language)
|
||
)
|
||
return
|
||
|
||
try:
|
||
status = await monitor_service.get_service_status()
|
||
|
||
status_text = "🔍 Статус сервиса мониторинга:\n\n"
|
||
status_text += f"🟢 Работает: {'Да' if status['is_running'] else 'Нет'}\n"
|
||
status_text += f"⏱ Интервал проверки: {status['check_interval']} сек\n"
|
||
status_text += f"🕙 Время ежедневной проверки: {status['daily_check_hour']}:00\n"
|
||
status_text += f"⚠️ Предупреждение за: {status['warning_days']} дней\n"
|
||
|
||
if status['last_check']:
|
||
status_text += f"🕐 Последняя проверка: {status['last_check']}"
|
||
|
||
await callback.message.edit_text(
|
||
status_text,
|
||
reply_markup=back_keyboard("admin_monitor", user.language)
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting monitor status: {e}")
|
||
await callback.message.edit_text(
|
||
"❌ Ошибка получения статуса",
|
||
reply_markup=back_keyboard("admin_monitor", user.language)
|
||
)
|
||
|
||
@admin_router.callback_query(F.data == "monitor_force_check")
|
||
async def monitor_force_check_callback(callback: CallbackQuery, user: User, **kwargs):
|
||
"""Force daily check"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
monitor_service = kwargs.get('monitor_service')
|
||
if not monitor_service:
|
||
await callback.answer("❌ Сервис мониторинга недоступен")
|
||
return
|
||
|
||
try:
|
||
await callback.answer("⏳ Запускаю принудительную проверку...")
|
||
await monitor_service.force_daily_check()
|
||
await callback.message.edit_text(
|
||
"✅ Принудительная проверка завершена",
|
||
reply_markup=back_keyboard("admin_monitor", user.language)
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error forcing check: {e}")
|
||
await callback.message.edit_text(
|
||
"❌ Ошибка при выполнении проверки",
|
||
reply_markup=back_keyboard("admin_monitor", user.language)
|
||
)
|
||
|
||
@admin_router.callback_query(F.data == "monitor_deactivate_expired")
|
||
async def monitor_deactivate_expired_callback(callback: CallbackQuery, user: User, **kwargs):
|
||
"""Deactivate expired subscriptions"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
monitor_service = kwargs.get('monitor_service')
|
||
if not monitor_service:
|
||
await callback.answer("❌ Сервис мониторинга недоступен")
|
||
return
|
||
|
||
try:
|
||
await callback.answer("⏳ Деактивирую истекшие подписки...")
|
||
count = await monitor_service.deactivate_expired_subscriptions()
|
||
|
||
await callback.message.edit_text(
|
||
f"✅ Деактивировано {count} истекших подписок",
|
||
reply_markup=back_keyboard("admin_monitor", user.language)
|
||
)
|
||
|
||
log_user_action(user.telegram_id, "expired_subscriptions_deactivated", f"Count: {count}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error deactivating expired subscriptions: {e}")
|
||
await callback.message.edit_text(
|
||
"❌ Ошибка при деактивации подписок",
|
||
reply_markup=back_keyboard("admin_monitor", user.language)
|
||
)
|
||
|
||
@admin_router.callback_query(F.data == "monitor_test_user")
|
||
async def monitor_test_user_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
|
||
"""Test monitor for specific user"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
"👤 Введите Telegram ID пользователя для тестирования уведомлений:",
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_test_monitor_user)
|
||
|
||
@admin_router.message(StateFilter(BotStates.admin_test_monitor_user))
|
||
async def handle_monitor_test_user(message: Message, state: FSMContext, user: User, **kwargs):
|
||
"""Handle user ID for monitor testing"""
|
||
telegram_id = parse_telegram_id(message.text)
|
||
|
||
if not telegram_id:
|
||
await message.answer("❌ Неверный Telegram ID")
|
||
return
|
||
|
||
monitor_service = kwargs.get('monitor_service')
|
||
if not monitor_service:
|
||
await message.answer("❌ Сервис мониторинга недоступен")
|
||
await state.clear()
|
||
return
|
||
|
||
try:
|
||
results = await monitor_service.check_single_user(telegram_id)
|
||
|
||
if not results:
|
||
await message.answer("❌ Результаты не получены")
|
||
else:
|
||
text = f"📊 Результаты тестирования для пользователя {telegram_id}:\n\n"
|
||
|
||
for result in results:
|
||
status = "✅" if result.success else "❌"
|
||
text += f"{status} {result.message}\n"
|
||
if result.error:
|
||
text += f" Ошибка: {result.error}\n"
|
||
|
||
await message.answer(
|
||
text,
|
||
reply_markup=admin_menu_keyboard(user.language)
|
||
)
|
||
|
||
log_user_action(user.telegram_id, "monitor_test_user", f"User: {telegram_id}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error testing monitor for user: {e}")
|
||
await message.answer("❌ Ошибка при тестировании")
|
||
|
||
await state.clear()
|
||
|
||
@admin_router.message(StateFilter(BotStates.admin_send_message_text))
|
||
async def handle_send_message(message: Message, state: FSMContext, user: User, **kwargs):
|
||
"""Handle message text input and send message"""
|
||
message_text = message.text.strip()
|
||
|
||
if len(message_text) < 1:
|
||
await message.answer("❌ Сообщение не может быть пустым")
|
||
return
|
||
|
||
data = await state.get_data()
|
||
target_user_id = data['target_user_id']
|
||
|
||
try:
|
||
bot = kwargs.get('bot')
|
||
if bot:
|
||
await bot.send_message(target_user_id, message_text)
|
||
await message.answer(
|
||
t('message_sent', user.language),
|
||
reply_markup=admin_menu_keyboard(user.language)
|
||
)
|
||
log_user_action(user.telegram_id, "message_sent", f"To user: {target_user_id}")
|
||
else:
|
||
await message.answer("❌ Ошибка отправки сообщения")
|
||
except Exception as e:
|
||
logger.error(f"Error sending message: {e}")
|
||
await message.answer("❌ Ошибка отправки сообщения (пользователь заблокировал бота?)")
|
||
|
||
await state.clear()
|
||
|
||
@admin_router.callback_query(F.data == "admin_send_to_all")
|
||
async def admin_send_to_all_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
|
||
"""Start broadcast message"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
await callback.message.edit_text(
|
||
t('enter_message_text', user.language),
|
||
reply_markup=cancel_keyboard(user.language)
|
||
)
|
||
await state.set_state(BotStates.admin_broadcast_text)
|
||
|
||
@admin_router.callback_query(F.data == "main_menu", StateFilter(BotStates.admin_test_monitor_user))
|
||
async def cancel_monitor_test(callback: CallbackQuery, state: FSMContext, user: User, **kwargs):
|
||
"""Cancel monitor test"""
|
||
await state.clear()
|
||
await callback.message.edit_text(
|
||
t('main_menu', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
|
||
@admin_router.message(StateFilter(BotStates.admin_broadcast_text))
|
||
async def handle_broadcast_message(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
|
||
"""Handle broadcast message"""
|
||
message_text = message.text.strip()
|
||
|
||
if len(message_text) < 1:
|
||
await message.answer("❌ Сообщение не может быть пустым")
|
||
return
|
||
|
||
try:
|
||
# Get all users
|
||
users = await db.get_all_users()
|
||
|
||
if not users:
|
||
await message.answer("❌ Пользователи не найдены")
|
||
await state.clear()
|
||
return
|
||
|
||
bot = kwargs.get('bot')
|
||
if not bot:
|
||
await message.answer("❌ Ошибка отправки сообщения")
|
||
await state.clear()
|
||
return
|
||
|
||
sent_count = 0
|
||
error_count = 0
|
||
|
||
# Show progress message
|
||
progress_msg = await message.answer(f"📤 Отправка сообщения {len(users)} пользователям...")
|
||
|
||
# Send to all users
|
||
for target_user in users:
|
||
try:
|
||
await bot.send_message(target_user.telegram_id, message_text)
|
||
sent_count += 1
|
||
except Exception as e:
|
||
logger.warning(f"Failed to send broadcast to {target_user.telegram_id}: {e}")
|
||
error_count += 1
|
||
|
||
# Small delay to avoid rate limits
|
||
await asyncio.sleep(0.05)
|
||
|
||
# Update progress message with results
|
||
await progress_msg.edit_text(
|
||
t('broadcast_sent', user.language) + "\n" +
|
||
t('broadcast_stats', user.language, sent=sent_count, errors=error_count),
|
||
reply_markup=admin_menu_keyboard(user.language)
|
||
)
|
||
|
||
log_user_action(user.telegram_id, "broadcast_sent", f"Sent: {sent_count}, Errors: {error_count}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error sending broadcast: {e}")
|
||
await message.answer(
|
||
t('error_occurred', user.language),
|
||
reply_markup=admin_menu_keyboard(user.language)
|
||
)
|
||
|
||
await state.clear()
|
||
|
||
# System management handlers
|
||
@admin_router.callback_query(F.data == "admin_system")
|
||
async def admin_system_callback(callback: CallbackQuery, user: User, **kwargs):
|
||
"""Show system management menu"""
|
||
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):
|
||
"""Show detailed system statistics"""
|
||
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):
|
||
"""Refresh system statistics"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
await callback.answer("🔄 Обновляю статистику...")
|
||
await show_system_stats(callback, user, db, api)
|
||
|
||
async def show_system_stats(callback: CallbackQuery, user: User, db: Database, api: RemnaWaveAPI = None, force_refresh: bool = False):
|
||
"""Display comprehensive system statistics with correct node status"""
|
||
try:
|
||
# Get database stats
|
||
db_stats = await db.get_stats()
|
||
current_time = datetime.now()
|
||
|
||
text = "📊 Системная статистика\n\n"
|
||
|
||
# Database statistics
|
||
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"
|
||
|
||
# RemnaWave API status
|
||
if api:
|
||
text += "\n🔗 API RemnaWave: 🟢 Подключен\n"
|
||
|
||
try:
|
||
logger.info("=== FETCHING SYSTEM STATS ===")
|
||
|
||
# Получаем статистику пользователей - ИСПРАВЛЕНО
|
||
await callback.answer("📊 Загружаю статистику пользователей...")
|
||
|
||
# Сначала пробуем получить полный список пользователей
|
||
all_users = await api.get_all_system_users_full()
|
||
logger.info(f"Got {len(all_users) if all_users else 0} users from get_all_system_users_full")
|
||
|
||
# Если не получилось, пробуем другой метод
|
||
if not all_users:
|
||
logger.warning("get_all_system_users_full returned empty, trying alternative method")
|
||
try:
|
||
# Пробуем получить через системную статистику
|
||
system_stats = await api.get_system_stats()
|
||
logger.info(f"System stats response: {system_stats}")
|
||
|
||
# Пробуем альтернативный метод получения пользователей
|
||
users_count = await api.get_users_count()
|
||
logger.info(f"Users count from API: {users_count}")
|
||
|
||
except Exception as alt_error:
|
||
logger.error(f"Alternative user fetching failed: {alt_error}")
|
||
|
||
# Получаем статистику нод
|
||
await callback.answer("🖥 Загружаю статистику нод...")
|
||
all_nodes = await api.get_all_nodes()
|
||
logger.info(f"Got {len(all_nodes) if all_nodes else 0} nodes from API")
|
||
|
||
text += "\n🖥 Система RemnaWave:\n"
|
||
|
||
# === ПОЛЬЗОВАТЕЛИ - ИСПРАВЛЕННАЯ ЛОГИКА ===
|
||
if all_users:
|
||
total_users = len(all_users)
|
||
active_users = len([u for u in all_users if str(u.get('status', '')).upper() == 'ACTIVE'])
|
||
inactive_users = total_users - active_users
|
||
|
||
text += f"👤 Пользователей в системе: {total_users}\n"
|
||
text += f"✅ Активных: {active_users}\n"
|
||
text += f"❌ Неактивных: {inactive_users}\n"
|
||
|
||
logger.info(f"Users stats: Total={total_users}, Active={active_users}, Inactive={inactive_users}")
|
||
else:
|
||
# Если список пользователей пустой, пробуем получить count
|
||
try:
|
||
users_count = await api.get_users_count()
|
||
if users_count is not None and users_count > 0:
|
||
text += f"👤 Пользователей в системе: {users_count}\n"
|
||
text += "⚠️ Детальная статистика недоступна\n"
|
||
else:
|
||
text += "👤 Пользователи: Нет данных или пустая система\n"
|
||
# Добавляем диагностическую информацию
|
||
text += "🔍 Возможные причины:\n"
|
||
text += "• Система только установлена\n"
|
||
text += "• Проблема с API доступом\n"
|
||
text += "• Ошибка в структуре данных API\n"
|
||
except Exception as count_error:
|
||
logger.error(f"Failed to get users count: {count_error}")
|
||
text += "👤 Пользователи: ❌ Ошибка получения данных\n"
|
||
|
||
# === НОДЫ ===
|
||
if all_nodes:
|
||
total_nodes = len(all_nodes)
|
||
online_nodes = 0
|
||
offline_nodes = 0
|
||
disabled_nodes = 0
|
||
|
||
text += f"\n📡 Ноды ({total_nodes} шт.):\n"
|
||
|
||
for i, node in enumerate(all_nodes):
|
||
node_name = node.get('name', f'Node-{i+1}')
|
||
status = node.get('status', 'unknown')
|
||
|
||
logger.debug(f"Node '{node_name}': status='{status}'")
|
||
|
||
if status == 'online':
|
||
online_nodes += 1
|
||
status_emoji = "🟢"
|
||
elif status == 'disabled':
|
||
disabled_nodes += 1
|
||
status_emoji = "⚫"
|
||
else:
|
||
offline_nodes += 1
|
||
status_emoji = "🔴"
|
||
|
||
# Показываем первые 5 нод
|
||
if i < 5:
|
||
display_name = node_name[:20] + "..." if len(node_name) > 20 else node_name
|
||
text += f"{status_emoji} {display_name}\n"
|
||
|
||
if total_nodes > 5:
|
||
text += f"... и еще {total_nodes - 5} нод\n"
|
||
|
||
text += f"\n🖥 Итого нод:\n"
|
||
text += f"• Всего: {total_nodes}\n"
|
||
text += f"• 🟢 Онлайн: {online_nodes}\n"
|
||
text += f"• 🔴 Оффлайн: {offline_nodes}\n"
|
||
if disabled_nodes > 0:
|
||
text += f"• ⚫ Отключено: {disabled_nodes}\n"
|
||
|
||
logger.info(f"Nodes stats: Total={total_nodes}, Online={online_nodes}, Offline={offline_nodes}, Disabled={disabled_nodes}")
|
||
|
||
# Определяем общее состояние системы
|
||
if online_nodes == total_nodes:
|
||
system_status = "🟢 Нормальное"
|
||
elif online_nodes == 0:
|
||
system_status = "🔴 Критическое"
|
||
elif online_nodes < total_nodes / 2:
|
||
system_status = "🟠 Система работает частично"
|
||
else:
|
||
system_status = "🟡 Предупреждение"
|
||
|
||
text += f"\n🏥 Состояние: {system_status}\n"
|
||
else:
|
||
text += "\n⚠️ Ноды: данные недоступны\n"
|
||
|
||
# === ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ ===
|
||
try:
|
||
# Пытаемся получить системную статистику для трафика
|
||
system_stats = await api.get_system_stats()
|
||
if system_stats and 'bandwidth' in system_stats:
|
||
bandwidth = system_stats['bandwidth']
|
||
|
||
# Ищем актуальные данные о трафике
|
||
if 'bandwidthCurrentYear' in bandwidth:
|
||
current_year = bandwidth['bandwidthCurrentYear'].get('current', '0')
|
||
if current_year != '0':
|
||
text += f"\n📊 Трафик за год: {current_year}\n"
|
||
|
||
if 'bandwidthCalendarMonth' in bandwidth:
|
||
current_month = bandwidth['bandwidthCalendarMonth'].get('current', '0')
|
||
if current_month != '0':
|
||
text += f"📊 Трафик за месяц: {current_month}\n"
|
||
|
||
except Exception as e:
|
||
logger.warning(f"Failed to get additional system stats: {e}")
|
||
|
||
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"
|
||
|
||
# Add timestamp
|
||
text += f"\n🕐 Обновлено: {format_datetime(current_time, user.language)}"
|
||
|
||
# Create keyboard
|
||
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)
|
||
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)
|
||
|
||
@admin_router.callback_query(F.data == "debug_users_api")
|
||
async def debug_users_api_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
|
||
"""Debug users API to check response structure"""
|
||
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 метод
|
||
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):
|
||
"""Comprehensive API debugging with detailed analysis"""
|
||
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):
|
||
"""Analyze nodes response for debugging"""
|
||
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"
|
||
|
||
# Показываем первые 2 ноды для примера
|
||
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):
|
||
"""Analyze users response for debugging"""
|
||
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):
|
||
"""Show improved nodes management interface"""
|
||
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):
|
||
"""Show nodes management with improved display and error handling"""
|
||
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("🖥 Загружаю информацию о нодах...")
|
||
|
||
# Get nodes with improved API call
|
||
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
|
||
|
||
# Calculate statistics
|
||
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)
|
||
|
||
# Build display text
|
||
text = "🖥 **Управление нодами**\n\n"
|
||
|
||
# Overall statistics
|
||
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"
|
||
|
||
# System health indicator
|
||
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"
|
||
|
||
# Show online nodes first
|
||
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"
|
||
|
||
# Show offline nodes
|
||
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"
|
||
|
||
# Show disabled nodes
|
||
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🕐 _Обновлено: {format_datetime(datetime.now(), user.language)}_"
|
||
|
||
# Create improved keyboard
|
||
keyboard = nodes_management_keyboard(nodes, user.language)
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=keyboard,
|
||
parse_mode='Markdown'
|
||
)
|
||
|
||
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:
|
||
"""Format node information for display"""
|
||
name = node.get('name', f'Node-{index}')
|
||
address = node.get('address', 'N/A')
|
||
|
||
# Truncate long values
|
||
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"
|
||
|
||
# Add resource usage if available
|
||
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"
|
||
|
||
# Add users count if available
|
||
if node.get('usersCount'):
|
||
text += f" 👥 Пользователей: {node['usersCount']}\n"
|
||
|
||
return text
|
||
|
||
@admin_router.callback_query(F.data == "restart_all_nodes")
|
||
async def restart_all_nodes_callback(callback: CallbackQuery, user: User, **kwargs):
|
||
"""Show confirmation for restarting all nodes"""
|
||
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):
|
||
"""Confirm and restart all nodes with improved error handling - ИСПРАВЛЕНО"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
try:
|
||
# Проверяем доступность API
|
||
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):
|
||
"""Show detailed node information"""
|
||
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
|
||
|
||
# Get all nodes and find the specific one
|
||
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
|
||
|
||
# Build detailed information
|
||
text = "🖥 **Детальная информация о ноде**\n\n"
|
||
|
||
text += f"📛 **Название:** {node.get('name', 'Unknown')}\n"
|
||
text += f"🆔 **ID:** `{node.get('id', node.get('uuid', 'N/A'))}`\n"
|
||
|
||
# Status with detailed info
|
||
status = node.get('status', 'unknown')
|
||
status_emoji = {
|
||
'online': '🟢',
|
||
'offline': '🔴',
|
||
'disabled': '⚫',
|
||
'disconnected': '🔴',
|
||
'xray_stopped': '🟡'
|
||
}.get(status, '⚪')
|
||
|
||
text += f"🔘 **Статус:** {status_emoji} {status.upper()}\n\n"
|
||
|
||
# Connection details
|
||
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"
|
||
|
||
# Address
|
||
if node.get('address'):
|
||
text += f"🌐 **Адрес:** `{node['address']}`\n\n"
|
||
|
||
# Resource usage
|
||
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"
|
||
|
||
# Users
|
||
if node.get('usersCount') is not None:
|
||
text += f"👥 **Пользователей:** {node['usersCount']}\n\n"
|
||
|
||
# Create action keyboard
|
||
keyboard = create_node_actions_keyboard(node_id, status, user.language)
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error showing node details: {e}")
|
||
await callback.answer("❌ Ошибка загрузки информации", show_alert=True)
|
||
|
||
def create_progress_bar(percent: float, length: int = 10) -> str:
|
||
"""Create a text progress bar"""
|
||
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:
|
||
"""Create keyboard for node actions"""
|
||
buttons = []
|
||
|
||
# Status control
|
||
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}")
|
||
])
|
||
|
||
# Restart button
|
||
buttons.append([
|
||
InlineKeyboardButton(text="🔄 Перезагрузить ноду", callback_data=f"restart_node_{node_id}")
|
||
])
|
||
|
||
# Refresh button
|
||
buttons.append([
|
||
InlineKeyboardButton(text="🔄 Обновить информацию", callback_data=f"refresh_node_{node_id}")
|
||
])
|
||
|
||
# Back button
|
||
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):
|
||
"""Enable specific node"""
|
||
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}")
|
||
|
||
# Refresh node details
|
||
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):
|
||
"""Disable specific node"""
|
||
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}")
|
||
|
||
# Refresh node details
|
||
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):
|
||
"""Show confirmation for restarting specific node"""
|
||
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):
|
||
"""Confirm and restart specific node"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
try:
|
||
node_id = callback.data.replace("confirm_restart_node_", "")
|
||
await callback.answer("🔄 Перезагружаю ноду...")
|
||
|
||
# Здесь нужно добавить метод restart_node в RemnaWaveAPI если его нет
|
||
# Пока используем заглушку
|
||
if api:
|
||
# result = await api.restart_node(node_id) # Метод нужно добавить в 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 == "system_users")
|
||
async def system_users_callback(callback: CallbackQuery, user: User, **kwargs):
|
||
"""Show system users management - ИСПРАВЛЕНО"""
|
||
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}")
|
||
# Если редактирование не удается, отвечаем на callback
|
||
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="✅ Обновлено"):
|
||
"""Безопасное редактирование сообщения с обработкой 'message is not modified'"""
|
||
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}")
|
||
# Попробуем просто ответить на callback
|
||
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):
|
||
"""Show bulk operations menu"""
|
||
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):
|
||
"""Reset traffic for all users"""
|
||
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):
|
||
"""Confirm bulk traffic reset"""
|
||
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_stats_callback
|
||
@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 with link to detailed system stats"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
try:
|
||
# Get database stats
|
||
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"
|
||
|
||
# Quick RemnaWave info
|
||
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):
|
||
"""List all system users with improved display"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
# Сброс состояния для пагинации
|
||
if state:
|
||
await state.clear()
|
||
await state.update_data(users_page=0)
|
||
|
||
# Проверяем наличие API
|
||
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):
|
||
"""Show paginated system users list with better formatting - ИСПРАВЛЕНО"""
|
||
try:
|
||
if not api:
|
||
await callback.message.edit_text(
|
||
"❌ API недоступен",
|
||
reply_markup=system_users_keyboard(user.language)
|
||
)
|
||
return
|
||
|
||
await callback.answer("📋 Загружаю список пользователей...")
|
||
|
||
# Get all users
|
||
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
|
||
|
||
# Sort users by status and creation date
|
||
all_users.sort(key=lambda x: (
|
||
0 if x.get('status') == 'ACTIVE' else 1,
|
||
x.get('createdAt', ''),
|
||
), reverse=True)
|
||
|
||
# Pagination
|
||
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]
|
||
|
||
# Statistics
|
||
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')])
|
||
|
||
# Build display text - БЕЗ MARKDOWN форматирования
|
||
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"
|
||
|
||
# Display users with improved formatting
|
||
for i, sys_user in enumerate(page_users, start=start_idx + 1):
|
||
# Status icon
|
||
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 = "⚪"
|
||
|
||
# User info - ОЧИЩАЕМ от специальных символов
|
||
username = sys_user.get('username', 'N/A')
|
||
# Удаляем или экранируем специальные символы Markdown
|
||
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" # Убрали ** для жирного текста
|
||
|
||
# Telegram info
|
||
if sys_user.get('telegramId'):
|
||
telegram_id = str(sys_user['telegramId'])
|
||
text += f" 📱 TG: {telegram_id}\n" # Убрали ` для моноширинного шрифта
|
||
|
||
# UUID info
|
||
text += f" 🔗 {short_uuid}\n"
|
||
|
||
# Expiry info
|
||
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 info
|
||
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"
|
||
|
||
# Create pagination keyboard
|
||
keyboard = create_users_pagination_keyboard(page, total_pages, user.language)
|
||
|
||
# Отправляем БЕЗ parse_mode
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=keyboard
|
||
# Убрали parse_mode='Markdown'
|
||
)
|
||
|
||
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:
|
||
"""Create pagination keyboard for users list"""
|
||
buttons = []
|
||
|
||
# Quick actions row
|
||
buttons.append([
|
||
InlineKeyboardButton(text="🔍 Поиск", callback_data="search_user_uuid"),
|
||
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_users_page_{current_page}")
|
||
])
|
||
|
||
# Pagination row
|
||
if total_pages > 1:
|
||
nav_row = []
|
||
|
||
# First page button
|
||
if current_page > 0:
|
||
nav_row.append(InlineKeyboardButton(text="⏮", callback_data="users_page_0"))
|
||
|
||
# Previous button
|
||
if current_page > 0:
|
||
nav_row.append(InlineKeyboardButton(text="◀️", callback_data=f"users_page_{current_page - 1}"))
|
||
|
||
# Current page indicator
|
||
nav_row.append(InlineKeyboardButton(text=f"{current_page + 1}/{total_pages}", callback_data="noop"))
|
||
|
||
# Next button
|
||
if current_page < total_pages - 1:
|
||
nav_row.append(InlineKeyboardButton(text="▶️", callback_data=f"users_page_{current_page + 1}"))
|
||
|
||
# Last page button
|
||
if current_page < total_pages - 1:
|
||
nav_row.append(InlineKeyboardButton(text="⏭", callback_data=f"users_page_{total_pages - 1}"))
|
||
|
||
buttons.append(nav_row)
|
||
|
||
# Filter buttons
|
||
buttons.append([
|
||
InlineKeyboardButton(text="✅ Только активные", callback_data="filter_users_active"),
|
||
InlineKeyboardButton(text="📱 С Telegram", callback_data="filter_users_telegram")
|
||
])
|
||
|
||
# Back button
|
||
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):
|
||
"""Handle users list pagination"""
|
||
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):
|
||
"""Handle refresh system users with timestamp"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
await show_system_users_list(callback, user, api, force_refresh=True)
|
||
|
||
# Helper function to create keyboards with timestamps
|
||
def system_stats_keyboard(language: str, timestamp: int = None) -> InlineKeyboardMarkup:
|
||
"""Create system stats keyboard with optional timestamp"""
|
||
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:
|
||
"""Create nodes management keyboard with optional timestamp"""
|
||
buttons = []
|
||
|
||
# Node action buttons
|
||
if nodes:
|
||
# Add individual node buttons (first 3)
|
||
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}"
|
||
)
|
||
])
|
||
|
||
# Restart all nodes button
|
||
buttons.append([
|
||
InlineKeyboardButton(text="🔄 Перезагрузить все ноды", callback_data="restart_all_nodes")
|
||
])
|
||
|
||
# Refresh button with timestamp if provided
|
||
refresh_callback = f"refresh_nodes_stats_{timestamp}" if timestamp else "refresh_nodes_stats"
|
||
buttons.append([
|
||
InlineKeyboardButton(text="🔄 Обновить", callback_data=refresh_callback)
|
||
])
|
||
|
||
# Back button
|
||
buttons.append([
|
||
InlineKeyboardButton(text="🔙 Назад", callback_data="admin_system")
|
||
])
|
||
|
||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||
|
||
@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):
|
||
"""Handle refresh nodes stats with timestamp"""
|
||
if not await check_admin_access(callback, user):
|
||
return
|
||
|
||
await show_nodes_management(callback, user, api, force_refresh=True)
|
||
|
||
@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):
|
||
"""Handle refresh system stats with timestamp"""
|
||
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):
|
||
"""Show detailed users statistics"""
|
||
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):
|
||
"""Start universal user search"""
|
||
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):
|
||
"""Handle universal user search"""
|
||
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
|
||
|
||
# Try different search methods
|
||
# 1. Check if it's a UUID
|
||
if validate_squad_uuid(search_input):
|
||
user_data = await api.get_user_by_uuid(search_input)
|
||
search_method = "UUID"
|
||
|
||
# 2. Try as Telegram ID
|
||
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
|
||
|
||
# 3. Try as Short UUID
|
||
if not user_data:
|
||
user_data = await api.get_user_by_short_uuid(search_input)
|
||
search_method = "Short UUID"
|
||
|
||
# 4. Try as Username
|
||
if not user_data:
|
||
user_data = await api.get_user_by_username(search_input)
|
||
search_method = "Username"
|
||
|
||
# 5. Try as Email
|
||
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
|
||
|
||
# Get local user info if exists
|
||
local_user = None
|
||
if user_data.get('telegramId') and db:
|
||
local_user = await db.get_user_by_telegram_id(user_data['telegramId'])
|
||
|
||
# Format user information
|
||
text = f"👤 Информация о пользователе\n"
|
||
text += f"🔍 Найден по: {search_method}\n\n"
|
||
|
||
# Basic info
|
||
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
|
||
status = user_data.get('status', 'UNKNOWN')
|
||
status_emoji = "✅" if status == 'ACTIVE' else "❌"
|
||
text += f"\n🔘 Статус: {status_emoji} {status}\n"
|
||
|
||
# Subscription info
|
||
if user_data.get('expireAt'):
|
||
expire_date = user_data['expireAt']
|
||
text += f"⏰ Истекает: {expire_date[:10]}\n"
|
||
|
||
# Calculate days left
|
||
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 info
|
||
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"
|
||
|
||
# Create management keyboard
|
||
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:
|
||
"""Create keyboard for user management"""
|
||
buttons = []
|
||
|
||
# Status control 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}")
|
||
])
|
||
|
||
# Edit buttons
|
||
buttons.append([
|
||
InlineKeyboardButton(text="📅 Изменить срок", callback_data=f"edit_user_expiry_{user_uuid}"),
|
||
InlineKeyboardButton(text="📊 Изменить трафик", callback_data=f"edit_user_traffic_{user_uuid}")
|
||
])
|
||
|
||
# Additional info
|
||
buttons.append([
|
||
InlineKeyboardButton(text="📈 Статистика", callback_data=f"user_usage_stats_{user_uuid}"),
|
||
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_user_{user_uuid}")
|
||
])
|
||
|
||
# Navigation
|
||
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):
|
||
"""Start editing user expiry date"""
|
||
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):
|
||
"""Handle user expiry date edit - ИСПРАВЛЕНО"""
|
||
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:
|
||
# Parse input
|
||
new_expiry = None
|
||
|
||
# Try as number of days
|
||
try:
|
||
days = int(input_value)
|
||
if days > 0:
|
||
new_expiry = datetime.now() + timedelta(days=days)
|
||
except ValueError:
|
||
# Try as date
|
||
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
|
||
|
||
# Update user in RemnaWave - правильный формат для API
|
||
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):
|
||
"""Handle user expiry date edit"""
|
||
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:
|
||
# Parse input
|
||
new_expiry = None
|
||
|
||
# Try as number of days
|
||
try:
|
||
days = int(input_value)
|
||
if days > 0:
|
||
new_expiry = datetime.now() + timedelta(days=days)
|
||
except ValueError:
|
||
# Try as date
|
||
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
|
||
|
||
# Update user in RemnaWave
|
||
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):
|
||
"""Start editing user traffic limit"""
|
||
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):
|
||
"""Handle user traffic limit edit"""
|
||
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
|
||
|
||
# Update user traffic limit
|
||
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):
|
||
"""Refresh user information"""
|
||
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("🔄 Обновляю информацию...")
|
||
|
||
# Get updated user data
|
||
user_data = await api.get_user_by_uuid(user_uuid)
|
||
if not user_data:
|
||
await callback.answer("❌ Пользователь не найден", show_alert=True)
|
||
return
|
||
|
||
# Format updated information (reuse the same format as in search)
|
||
text = f"👤 Информация о пользователе (обновлено)\n\n"
|
||
# ... (same formatting as in search result)
|
||
|
||
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)
|
||
|
||
# Синхронизация с RemnaWave
|
||
@admin_router.callback_query(F.data == "sync_remnawave")
|
||
async def sync_remnawave_callback(callback: CallbackQuery, user: User, **kwargs):
|
||
"""Show RemnaWave synchronization menu"""
|
||
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:
|
||
"""Keyboard for RemnaWave sync options"""
|
||
buttons = [
|
||
[InlineKeyboardButton(text="👥 Синхронизировать пользователей", callback_data="sync_users_remnawave")],
|
||
[InlineKeyboardButton(text="📋 Синхронизировать подписки", callback_data="sync_subscriptions_remnawave")],
|
||
[InlineKeyboardButton(text="🔄 Полная синхронизация", callback_data="sync_full_remnawave")],
|
||
[InlineKeyboardButton(text="🌍 ИМПОРТ ВСЕХ по Telegram ID", callback_data="import_all_by_telegram")],
|
||
[InlineKeyboardButton(text="👤 Синхронизировать одного", callback_data="sync_single_user")],
|
||
[InlineKeyboardButton(text="📋 Просмотр планов", callback_data="view_imported_plans")],
|
||
[InlineKeyboardButton(text="🔍 Отладка всех планов", callback_data="debug_all_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):
|
||
"""Sync users between bot and RemnaWave"""
|
||
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("🔄 Запускаю синхронизацию пользователей...")
|
||
|
||
# Show progress message
|
||
progress_msg = await callback.message.edit_text("⏳ Синхронизация пользователей...\n\n0% выполнено")
|
||
|
||
# Get all users from RemnaWave
|
||
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:
|
||
# Update progress every 10 users
|
||
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
|
||
|
||
# Check if user exists in bot database
|
||
bot_user = await db.get_user_by_telegram_id(telegram_id)
|
||
|
||
if not bot_user:
|
||
# Create new user in bot database
|
||
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
|
||
|
||
# Update user's RemnaWave UUID if not set
|
||
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
|
||
|
||
# Final result
|
||
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):
|
||
"""Sync subscriptions between bot and RemnaWave - УЛУЧШЕННАЯ ВЕРСИЯ с логированием"""
|
||
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: Получение данных...")
|
||
|
||
# Получаем всех пользователей из RemnaWave
|
||
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
|
||
|
||
# Этап 1: Создание пользователей бота если их нет
|
||
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'), # Используем username как first_name
|
||
language='ru',
|
||
is_admin=is_admin
|
||
)
|
||
created_users += 1
|
||
logger.info(f"Created bot user for Telegram ID: {telegram_id}")
|
||
|
||
# Обновляем RemnaWave UUID если не установлен
|
||
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}")
|
||
|
||
# Этап 2: Синхронизация подписок
|
||
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
|
||
|
||
# Проверяем есть ли у пользователя активная подписка в RemnaWave
|
||
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
|
||
|
||
# Ищем подписку в боте по short_uuid
|
||
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'])
|
||
|
||
# Конвертируем в naive datetime для БД
|
||
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
|
||
squad_uuid = None
|
||
active_squads = remna_user.get('activeInternalSquads', [])
|
||
|
||
if active_squads:
|
||
# Берем первый активный squad
|
||
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:
|
||
# Fallback: берем из internalSquads
|
||
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:
|
||
# Ищем существующий план с таким 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, # Цена неизвестна, ставим 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:
|
||
# Fallback: 30 дней от сегодня
|
||
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
|
||
|
||
# Этап 3: Проверка консистентности
|
||
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)
|
||
|
||
# Также деактивируем в RemnaWave
|
||
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}")
|
||
|
||
# Этап 4: Финальная проверка
|
||
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):
|
||
"""Reset traffic for specific user with confirmation"""
|
||
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):
|
||
"""Disable specific user with confirmation"""
|
||
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):
|
||
"""Enable specific user with confirmation"""
|
||
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):
|
||
"""Show synchronization status"""
|
||
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("📊 Проверяю статус синхронизации...")
|
||
|
||
# Get statistics
|
||
remna_users = await api.get_all_system_users_full()
|
||
bot_users = await db.get_all_users()
|
||
|
||
# Count statistics
|
||
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
|
||
|
||
# Check subscriptions sync
|
||
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:
|
||
# Check if subscription exists in RemnaWave
|
||
for remna_user in remna_users:
|
||
if remna_user.get('shortUuid') == user_sub.short_uuid:
|
||
synced_subs += 1
|
||
break
|
||
|
||
# Build status text
|
||
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"
|
||
|
||
# Recommendations
|
||
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)
|
||
)
|
||
|
||
# User filtering handlers
|
||
@admin_router.callback_query(F.data == "filter_users_active")
|
||
async def filter_users_active_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
|
||
"""Show only active users - ИСПРАВЛЕНО"""
|
||
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
|
||
|
||
# Display filtered users - БЕЗ MARKDOWN
|
||
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 = 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} активных пользователей"
|
||
|
||
# Create keyboard with clear filter button
|
||
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
|
||
# Убрали parse_mode='Markdown'
|
||
)
|
||
|
||
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):
|
||
"""Show only users with Telegram ID"""
|
||
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
|
||
|
||
# Display filtered users
|
||
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_"
|
||
|
||
# Create keyboard
|
||
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):
|
||
"""Show all nodes with pagination"""
|
||
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):
|
||
"""Show paginated nodes list"""
|
||
try:
|
||
nodes = await api.get_all_nodes()
|
||
if not nodes:
|
||
await callback.message.edit_text(
|
||
"❌ Ноды не найдены",
|
||
reply_markup=admin_system_keyboard(user.language)
|
||
)
|
||
return
|
||
|
||
# Sort nodes by status
|
||
nodes.sort(key=lambda x: (
|
||
0 if x.get('status') == 'online' else 1,
|
||
x.get('name', '')
|
||
))
|
||
|
||
# Pagination
|
||
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]
|
||
|
||
# Build text
|
||
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"
|
||
|
||
# Create pagination keyboard
|
||
buttons = []
|
||
|
||
# Navigation
|
||
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):
|
||
"""Handle nodes pagination"""
|
||
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):
|
||
"""Full synchronization between bot and RemnaWave - УЛУЧШЕННАЯ ВЕРСИЯ"""
|
||
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
|
||
|
||
# Этап 1: Синхронизация пользователей
|
||
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}")
|
||
|
||
# Обновляем RemnaWave UUID
|
||
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
|
||
|
||
# Этап 2: Создание планов подписок
|
||
await progress_msg.edit_text(
|
||
"⏳ Полная синхронизация RemnaWave\n\n"
|
||
"Этап 2/5: Создание планов подписок..."
|
||
)
|
||
|
||
# Собираем уникальные squad_uuid из RemnaWave
|
||
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")
|
||
|
||
# Создаем планы для отсутствующих squad_uuid
|
||
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
|
||
|
||
# Этап 3: Синхронизация подписок
|
||
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
|
||
|
||
# Этап 4: Обновление статусов
|
||
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
|
||
|
||
# Обновляем в RemnaWave
|
||
if bot_user.remnawave_uuid:
|
||
try:
|
||
await api.update_user(bot_user.remnawave_uuid, {'status': 'EXPIRED'})
|
||
except:
|
||
pass
|
||
|
||
# Этап 5: Финальная статистика
|
||
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):
|
||
"""Start single user sync"""
|
||
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):
|
||
"""Handle single user sync - ИСПРАВЛЕНО"""
|
||
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("🔄 Синхронизирую пользователя...")
|
||
|
||
# Получаем данные из RemnaWave - ИСПРАВЛЕНИЕ
|
||
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
|
||
|
||
# Обрабатываем разные типы ответов от API
|
||
if isinstance(remna_user_result, dict):
|
||
remna_user = remna_user_result
|
||
elif isinstance(remna_user_result, list):
|
||
# Если API вернул список, берем первого пользователя
|
||
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("ℹ️ Пользователь уже существует в боте")
|
||
|
||
# Обновляем RemnaWave UUID
|
||
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 - ИСПРАВЛЕНИЕ
|
||
squad_uuid = None
|
||
|
||
# Проверяем activeInternalSquads
|
||
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
|
||
|
||
# Если не нашли, проверяем internalSquads
|
||
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"
|
||
|
||
# Информация о squad
|
||
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):
|
||
"""Import ALL subscriptions from RemnaWave by Telegram ID - ИСПРАВЛЕННАЯ ВЕРСИЯ для множественных подписок"""
|
||
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..."
|
||
)
|
||
|
||
# Получаем ВСЕХ пользователей из 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")
|
||
|
||
# Фильтруем только записи с Telegram ID
|
||
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")
|
||
|
||
# Группируем по 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
|
||
|
||
# Этап 1: Создание пользователей бота (по уникальным Telegram ID)
|
||
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}")
|
||
|
||
# Обновляем RemnaWave UUID (используем последний)
|
||
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
|
||
|
||
# Этап 2: Сбор всех уникальных squad UUID
|
||
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')}")
|
||
|
||
# Извлекаем squad UUID из activeInternalSquads
|
||
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)}")
|
||
|
||
# Этап 3: Создание планов подписок
|
||
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")
|
||
|
||
# Этап 4: Импорт каждой подписки отдельно
|
||
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}")
|
||
|
||
# Пропускаем если нет shortUuid
|
||
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 из activeInternalSquads
|
||
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
|
||
|
||
# Этап 5: Финальная статистика
|
||
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):
|
||
"""Handle user structure debugging"""
|
||
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"
|
||
|
||
# Squad поля
|
||
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"
|
||
|
||
# Детальный анализ squad полей
|
||
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):
|
||
"""Rename all imported subscription plans to 'Старая подписка'"""
|
||
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
|
||
|
||
# Пропускаем настоящие триальные планы (помеченные как is_trial = True)
|
||
if getattr(plan, 'is_trial', False):
|
||
logger.debug(f"Skipping trial plan: {plan.name}")
|
||
continue
|
||
|
||
# Критерии импортированного плана:
|
||
is_imported_plan = False
|
||
|
||
# 1. Явно помеченные как импортированные
|
||
if getattr(plan, 'is_imported', False):
|
||
is_imported_plan = True
|
||
logger.debug(f"Plan {plan.name} marked as imported")
|
||
|
||
# 2. Планы с автогенерированными названиями для импорта
|
||
elif plan.name.startswith(('Import_', 'Auto_', 'Imported_')):
|
||
is_imported_plan = True
|
||
logger.debug(f"Plan {plan.name} has import prefix")
|
||
|
||
# 3. Планы с названиями Trial_ которые НЕ являются настоящими триальными
|
||
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")
|
||
|
||
# 4. Планы с подозрительными характеристиками импорта
|
||
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")
|
||
|
||
# 5. Планы с squad в описании (характерно для импорта)
|
||
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]: # Показываем первые 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")
|
||
]
|
||
])
|
||
|
||
# Сохраняем список планов в состояние FSM
|
||
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):
|
||
"""Confirm renaming of found plans"""
|
||
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):
|
||
"""Cancel rename and return to main menu"""
|
||
await state.clear()
|
||
await callback.message.edit_text(
|
||
t('main_menu', user.language),
|
||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||
)
|
||
|
||
@admin_router.callback_query(F.data == "view_imported_plans")
|
||
async def view_imported_plans_callback(callback: CallbackQuery, user: User, db: Database = None, **kwargs):
|
||
"""View all imported subscription plans"""
|
||
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):
|
||
"""Delete all imported plans with confirmation"""
|
||
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):
|
||
"""Confirm deletion of imported plans with proper cleanup"""
|
||
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):
|
||
"""Debug all subscription plans"""
|
||
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)
|