diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py index 39109027..5f676eed 100644 --- a/handlers/admin/__init__.py +++ b/handlers/admin/__init__.py @@ -5,7 +5,7 @@ from aiogram import Router from .admin_coupons import router as coupons_router from .admin_panel import router as panel_router from .admin_servers import router as servers_router -from .admin_user_editor import router as user_editor_router +from .admin_users_editor import router as user_editor_router router = Router(name="admins_main_router") diff --git a/handlers/admin/admin_users_editor.py b/handlers/admin/admin_users_editor.py new file mode 100644 index 00000000..182c3599 --- /dev/null +++ b/handlers/admin/admin_users_editor.py @@ -0,0 +1,596 @@ +import asyncio +from datetime import datetime +from typing import Any + +from aiogram import Bot, F, Router, types +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from aiogram.types import CallbackQuery +from config import TOTAL_GB + +from database import delete_user_data, get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry +from filters.admin import IsAdminFilter +from handlers.keys.key_utils import ( + delete_key_from_cluster, + delete_key_from_db, + renew_key_in_cluster, +) +from handlers.utils import sanitize_key_name +from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb +from keyboards.admin.users_editor_kb import build_user_edit_kb, build_key_edit_kb, build_key_delete_kb, \ + build_user_delete_kb, AdminUserEditorCallback +from logger import logger + +router = Router() + + +class UserEditorState(StatesGroup): + # search + waiting_for_user_data = State() + waiting_for_key_name = State() + # updating data + waiting_for_new_balance = State() + waiting_for_expiry_time = State() + waiting_for_message_text = State() + + +@router.callback_query( + AdminPanelCallback.filter(F.action == "users_search"), + IsAdminFilter(), +) +async def handle_users_search( + callback_query: CallbackQuery, + state: FSMContext +): + text = ( + "🔍 Введите ID или Username пользователя для поиска:" + "\n— ID - числовой айди пользователя" + "\n— Username - юзернейм пользователя, начинающийся с @ или https://t.me/" + ) + + await state.set_state(UserEditorState.waiting_for_user_data) + await callback_query.message.answer( + text=text, + reply_markup=build_admin_back_kb("users_editor") + ) + + +@router.callback_query( + AdminPanelCallback.filter(F.action == "users_search_key"), + IsAdminFilter(), +) +async def handle_users_search_key( + callback_query: CallbackQuery, + state: FSMContext +): + await state.set_state(UserEditorState.waiting_for_key_name) + await callback_query.message.answer( + text="🔑 Введите имя ключа для поиска:", + reply_markup=build_admin_back_kb("users_editor") + ) + + +@router.message( + UserEditorState.waiting_for_user_data, + IsAdminFilter() +) +async def handle_user_data_input( + message: types.Message, + state: FSMContext, + session: Any +): + kb = build_admin_back_kb("users_editor") + + if not message.text: + await message.reply( + text="💢 Пожалуйста, отправьте текстовое сообщение.", + reply_markup=kb + ) + return + + if message.text.isdigit(): + tg_id = int(message.text) + else: + # Удаление '@' символа в начале сообщения + username = message.text.strip().lstrip('@') + # Удаление начала ссылки на профиль + username = username.replace('https://t.me/', '') + + user = await session.fetchrow( + "SELECT tg_id FROM users WHERE username = $1", username + ) + + if not user: + await message.answer( + text="🔍 Пользователь с указанным username не найден. 🚫", + reply_markup=kb, + ) + await state.clear() + return + + tg_id = user["tg_id"] + + await process_user_search(message, state, session, tg_id) + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_send_message"), + IsAdminFilter(), +) +async def handle_send_message( + callback_query: types.CallbackQuery, + callback_data: AdminUserEditorCallback, + state: FSMContext +): + tg_id = callback_data.data + await callback_query.message.answer( + "✉️ Введите текст сообщения, которое вы хотите отправить пользователю." + ) + await state.update_data(target_tg_id=tg_id) + await state.set_state(UserEditorState.waiting_for_message_text) + + +@router.message( + UserEditorState.waiting_for_message_text, + IsAdminFilter() +) +async def handle_message_text_input( + message: types.Message, + state: FSMContext, + bot: Bot +): + data = await state.get_data() + target_tg_id = data.get("target_tg_id") + + if not target_tg_id: + await message.answer("🚫 Ошибка: ID пользователя не найден.") + await state.clear() + return + + try: + await bot.send_message(chat_id=target_tg_id, text=message.text) + await message.answer("✅ Сообщение успешно отправлено.") + except Exception as e: + await message.answer(f"❌ Не удалось отправить сообщение: {e}") + + await state.clear() + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_trial_restore"), + IsAdminFilter(), +) +async def handle_restore_trial( + callback_query: types.CallbackQuery, + callback_data: AdminUserEditorCallback, + session: Any +): + tg_id = int(callback_data.data) + await restore_trial(tg_id, session) + await callback_query.message.answer( + text="✅ Триал успешно восстановлен.", + reply_markup=build_admin_back_kb("admin") + ) + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_balance_change"), + IsAdminFilter() +) +async def process_balance_change( + callback_query: CallbackQuery, + callback_data: AdminUserEditorCallback, + state: FSMContext +): + tg_id = int(callback_data.data) + await state.update_data(tg_id=tg_id) + await callback_query.message.answer( + text="💸 Введите новую сумму баланса:", + reply_markup=build_admin_back_kb("users_editor") + ) + await state.set_state(UserEditorState.waiting_for_new_balance) + + +@router.message( + UserEditorState.waiting_for_new_balance, + IsAdminFilter() +) +async def handle_new_balance_input( + message: types.Message, + state: FSMContext, + session: Any +): + if not message.text.isdigit() or int(message.text) < 0: + await message.answer( + text="❌ Пожалуйста, введите корректную сумму для изменения баланса.", + reply_markup=build_admin_back_kb("users_editor"), + ) + return + + new_balance = int(message.text) + user_data = await state.get_data() + tg_id = user_data.get("tg_id") + + await session.execute( + "UPDATE connections SET balance = $1 WHERE tg_id = $2", + new_balance, + tg_id, + ) + + await message.answer( + text=f"✅ Баланс успешно изменен на {new_balance}.", + reply_markup=build_admin_back_kb("admin") + ) + await state.clear() + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_key_edit"), + IsAdminFilter() +) +async def process_key_edit( + callback_query: CallbackQuery, + callback_data: AdminUserEditorCallback, + session: Any +): + email = callback_data.data + key_details = await get_key_details(email, session) + + if not key_details: + await callback_query.message.answer( + text="🔍 Информация о ключе не найдена. 🚫", + reply_markup=build_admin_back_kb("users_editor"), + ) + return + + response_message = ( + f"🔑 Ключ: {key_details['key']}\n" + f"⏰ Дата истечения: {key_details['expiry_date']}\n" + f"💰 Баланс пользователя: {key_details['balance']}\n" + f"🌐 Кластер: {key_details['server_name']}" + ) + + await callback_query.message.answer( + text=response_message, + reply_markup=build_key_edit_kb(key_details, email) + ) + + +@router.message( + UserEditorState.waiting_for_key_name, + IsAdminFilter() +) +async def handle_key_name_input( + message: types.Message, + state: FSMContext, + session: Any +): + key_name = sanitize_key_name(message.text) + key_details = await get_key_details(key_name, session) + + if not key_details: + await message.answer( + text="🚫 Пользователь с указанным именем ключа не найден.", + reply_markup=build_admin_back_kb("users_editor") + ) + await state.clear() + return + + response_message = ( + f"🔑 Ключ: {key_details['key']}\n" + f"⏰ Дата истечения: {key_details['expiry_date']}\n" + f"💰 Баланс пользователя: {key_details['balance']}\n" + f"🌐 Сервер: {key_details['server_name']}" + ) + + await message.answer( + text=response_message, + reply_markup=build_key_edit_kb(key_details, key_name) + ) + await state.clear() + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_change_expiry"), + IsAdminFilter() +) +async def prompt_expiry_change( + callback_query: CallbackQuery, + callback_data: AdminUserEditorCallback, + state: FSMContext +): + email = callback_data.data + await callback_query.message.answer( + text=f"⏳ Введите новое время истечения для ключа {email} в формате YYYY-MM-DD HH:MM:SS:" + ) + await state.update_data(email=email) + await state.set_state(UserEditorState.waiting_for_expiry_time) + + +@router.message( + UserEditorState.waiting_for_expiry_time, + IsAdminFilter() +) +async def handle_expiry_time_input( + message: types.Message, + state: FSMContext, + session: Any +): + user_data = await state.get_data() + email = user_data.get("email") + + if not email: + await message.answer( + text="📧 Email не найден в состоянии. 🚫", + reply_markup=build_admin_back_kb("users_editor") + ) + await state.clear() + return + + try: + expiry_time_str = message.text + expiry_time = int( + datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").timestamp() * 1000 + ) + + client_id = await get_client_id_by_email(email) + if client_id is None: + await message.answer( + text=f"🚫 Клиент с email {email} не найден. 🔍", + reply_markup=build_admin_back_kb("users_editor"), + ) + await state.clear() + return + + record = await session.fetchrow( + "SELECT server_id FROM keys WHERE client_id = $1", client_id + ) + if not record: + await message.answer( + text="🚫 Клиент не найден в базе данных. 🔍", + reply_markup=build_admin_back_kb("users_editor"), + ) + await state.clear() + return + + clusters = await get_servers_from_db() + + async def update_key_on_all_servers(): + tasks = [ + asyncio.create_task( + renew_key_in_cluster( + cluster_name, + email, + client_id, + expiry_time, + total_gb=TOTAL_GB, + ) + ) + for cluster_name in clusters + ] + + await asyncio.gather(*tasks) + + await update_key_on_all_servers() + + await update_key_expiry(client_id, expiry_time) + + response_message = f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах." + + await message.answer( + text=response_message, + reply_markup=build_admin_back_kb("admin") + ) + except ValueError: + await message.answer( + text="❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.", + reply_markup=build_admin_back_kb("users_editor"), + ) + except Exception as e: + logger.error(e) + await state.clear() + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_delete_key"), + IsAdminFilter() +) +async def process_callback_delete_key( + callback_query: types.CallbackQuery, + callback_data: AdminUserEditorCallback, + session: Any +): + email = callback_data.data + client_id = await session.fetchval( + "SELECT client_id FROM keys WHERE email = $1", email + ) + + if client_id is None: + await callback_query.message.answer( + text="🔍 Ключ не найден. 🚫", + reply_markup=build_admin_back_kb("users_editor") + ) + return + + await callback_query.message.answer( + text="❓ Вы уверены, что хотите удалить ключ?", + reply_markup=build_key_delete_kb(client_id), + ) + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_delete_key_confirm"), + IsAdminFilter() +) +async def process_callback_confirm_delete( + callback_query: types.CallbackQuery, + callback_data: AdminUserEditorCallback, + session: Any +): + client_id = callback_data.data + record = await session.fetchrow( + "SELECT email FROM keys WHERE client_id = $1", client_id + ) + + kb = build_admin_back_kb("users_editor") + + if record: + clusters = await get_servers_from_db() + + async def delete_key_from_servers(email, client_id): + tasks = [] + for cluster_name, cluster_servers in clusters.items(): + for server in cluster_servers: + tasks.append( + delete_key_from_cluster(cluster_name, email, client_id) + ) + await asyncio.gather(*tasks) + + await delete_key_from_servers(record["email"], client_id) + await delete_key_from_db(client_id, session) + + await callback_query.message.answer( + text="✅ Ключ успешно удален.", + reply_markup=kb + ) + else: + await callback_query.message.answer( + text="🚫 Ключ не найден или уже удален.", + reply_markup=kb + ) + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_delete_user"), + IsAdminFilter() +) +async def delete_user( + callback_query: types.CallbackQuery, + callback_data: AdminUserEditorCallback, + session: Any +): + tg_id = int(callback_data.data) + key_records = await session.fetch("SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id) + + async def delete_keys_from_servers(): + try: + tasks = [] + for email, client_id in key_records: + servers = await get_servers_from_db() + for cluster_id, cluster in servers.items(): + tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) + await asyncio.gather(*tasks) + except Exception as e: + logger.error(f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}") + + await delete_keys_from_servers() + + try: + await delete_user_data(session, tg_id) + await callback_query.message.answer( + text=f"🗑️ Пользователь с ID {tg_id} был удален.", + reply_markup=build_admin_back_kb("users_editor") + ) + except Exception as e: + logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}") + await callback_query.message.answer( + text=f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова." + ) + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_delete_user_confirm"), + IsAdminFilter() +) +async def confirm_delete_user( + callback_query: types.CallbackQuery, + callback_data: AdminUserEditorCallback +): + tg_id = int(callback_data.data) + await callback_query.message.answer( + text=f"Вы уверены, что хотите удалить пользователя с ID {tg_id}?", + reply_markup=build_user_delete_kb(tg_id) + ) + + +async def process_user_search(message: types.Message, state: FSMContext, session: Any, tg_id: int) -> None: + username = await session.fetchval( + "SELECT username FROM users WHERE tg_id = $1", tg_id + ) + balance = await session.fetchval( + "SELECT balance FROM connections WHERE tg_id = $1", tg_id + ) + + if balance is None: + await message.answer( + text="🚫 Пользователь с указанным ID не найден!", + reply_markup=build_admin_back_kb("users_editor"), + ) + return + + key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) + referral_count = await session.fetchval( + "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id + ) + + text = ( + f"📊 Информация о пользователе:\n\n" + f"🆔 ID пользователя: {tg_id}\n" + f"👤 Логин пользователя: @{username}\n" + f"💰 Баланс: {balance}\n" + f"👥 Количество рефералов: {referral_count}\n" + f"🔑 Ключи (для редактирования нажмите на ключ):" + ) + + await message.answer( + text=text, + reply_markup=build_user_edit_kb(tg_id, key_records) + ) + + await state.clear() + + +async def get_key_details(email, session): + record = await session.fetchrow( + """ + SELECT k.key, k.expiry_time, k.server_id, c.tg_id, c.balance + FROM keys k + JOIN connections c ON k.tg_id = c.tg_id + WHERE k.email = $1 + """, + email, + ) + + if not record: + return None + + servers = await get_servers_from_db() + + cluster_name = "Неизвестный кластер" + for cluster_name, cluster_servers in servers.items(): + if any( + server["inbound_id"] == record["server_id"] for server in cluster_servers + ): + cluster_name = cluster_name + break + + expiry_date = datetime.utcfromtimestamp(record["expiry_time"] / 1000) + current_date = datetime.utcnow() + time_left = expiry_date - current_date + + if time_left.total_seconds() <= 0: + days_left_message = "Ключ истек." + elif time_left.days > 0: + days_left_message = f"Осталось дней: {time_left.days}" + else: + hours_left = time_left.seconds // 3600 + days_left_message = f"Осталось часов: {hours_left}" + + return { + "key": record["key"], + "expiry_date": expiry_date.strftime("%d %B %Y года"), + "days_left_message": days_left_message, + "server_name": cluster_name, + "balance": record["balance"], + "tg_id": record["tg_id"], + } diff --git a/keyboards/admin/users_editor_kb.py b/keyboards/admin/users_editor_kb.py new file mode 100644 index 00000000..f93e2dcd --- /dev/null +++ b/keyboards/admin/users_editor_kb.py @@ -0,0 +1,116 @@ +from aiogram.filters.callback_data import CallbackData +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from keyboards.admin.panel_kb import build_admin_back_btn + + +class AdminUserEditorCallback(CallbackData, prefix='admin_users_editor'): + action: str + data: str + + +def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + for email in key_records: + builder.button( + text=f"🔑 {email}", + callback_data=AdminUserEditorCallback( + action="users_key_edit", + data=email + ).pack() + ) + + builder.button( + text="📝 Изменить баланс", + callback_data=AdminUserEditorCallback( + action="users_balance_change", + data=tg_id + ).pack() + ) + builder.button( + text="🔄 Восстановить пробник", + callback_data=AdminUserEditorCallback( + action="users_trial_restore", + data=tg_id + ).pack() + ) + builder.button( + text="❌ Удалить клиента", + callback_data=AdminUserEditorCallback( + action="users_delete_user", + data=tg_id + ).pack() + ) + builder.row( + build_admin_back_btn("users_editor") + ) + return builder.as_markup() + + +def build_user_delete_kb(tg_id: int): + builder = InlineKeyboardBuilder() + builder.button( + text="❌ Удалить клиента", + callback_data=AdminUserEditorCallback( + action="users_delete_user_confirm", + data=tg_id + ).pack() + ) + builder.row( + build_admin_back_btn("users_editor") + ) + return builder.as_markup() + + +def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="ℹ️ Получить информацию о юзере", + callback_data=AdminUserEditorCallback( + action="users_info", + data=key_details["tg_id"] + ).pack() + ) + ) + builder.row( + InlineKeyboardButton( + text="⏳ Изменить время истечения", + callback_data=AdminUserEditorCallback( + action="users_change_expiry", + data=email + ).pack() + ) + ) + builder.row( + InlineKeyboardButton( + text="❌ Удалить ключ", + callback_data=AdminUserEditorCallback( + action="users_delete_key", + data=email + ).pack() + ) + ) + builder.row( + build_admin_back_btn("users_editor") + ) + return builder.as_markup() + + +def build_key_delete_kb(client_id: str) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="✅ Да, удалить", + callback_data=AdminUserEditorCallback( + action="users_delete_key_confirm", + data=client_id + ).pack() + ) + ) + builder.row( + build_admin_back_btn("users_editor") + ) + return builder.as_markup()