diff --git a/bot.py b/bot.py index db72dba1..abf95cb9 100644 --- a/bot.py +++ b/bot.py @@ -20,7 +20,7 @@ dp = Dispatcher(bot=bot, storage=storage) router = Router() from handlers import coupons, donate, notifications, pay, profile, start -from handlers.admin import admin_coupons, admin_panel, admin_user_editor +from handlers.admin import admin_coupons, admin_panel, admin_servers, admin_user_editor from handlers.instructions import instructions from handlers.keys import key_management, keys from handlers.payments import cryprobot_pay, freekassa_pay, robokassa_pay, stars_pay, yookassa_pay @@ -28,6 +28,7 @@ from handlers.payments import cryprobot_pay, freekassa_pay, robokassa_pay, stars dp.include_router(admin_coupons.router) dp.include_router(admin_panel.router) dp.include_router(admin_user_editor.router) +dp.include_router(admin_servers.router) dp.include_router(coupons.router) dp.include_router(start.router) dp.include_router(profile.router) diff --git a/database.py b/database.py index be587d4c..f1ad9c2d 100644 --- a/database.py +++ b/database.py @@ -1187,7 +1187,6 @@ async def check_notification_time(tg_id: int, notification_type: str, hours: int async def get_servers_from_db(): conn = await asyncpg.connect(DATABASE_URL) - # Запрос к базе данных для получения всех серверов result = await conn.fetch( """ SELECT cluster_name, server_name, api_url, subscription_url, inbound_id @@ -1197,7 +1196,6 @@ async def get_servers_from_db(): await conn.close() - # Преобразуем результат в удобный формат servers = {} for row in result: cluster_name = row['cluster_name'] diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index b1a656b6..b0adf30a 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -37,6 +37,7 @@ async def handle_admin_message(message: types.Message, state: FSMContext): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="📊 Статистика пользователей", callback_data="user_stats")) builder.row(InlineKeyboardButton(text="👥 Управление пользователями", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="🖥️ Управление серверами", callback_data="servers_editor")) builder.row(InlineKeyboardButton(text="🎟️ Управление купонами", callback_data="coupons_editor")) builder.row(InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to_alls")) builder.row(InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups")) diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py new file mode 100644 index 00000000..e4c518f9 --- /dev/null +++ b/handlers/admin/admin_servers.py @@ -0,0 +1,354 @@ +from aiogram import F, Router, types +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from aiogram.types import InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder +import asyncpg + +from config import DATABASE_URL +from database import get_servers_from_db +from filters.admin import IsAdminFilter + +router = Router() + + +class UserEditorState(StatesGroup): + waiting_for_cluster_name = State() + waiting_for_api_url = State() + waiting_for_inbound_id = State() + waiting_for_server_name = State() + waiting_for_subscription_url = State() + + +@router.callback_query(F.data == "servers_editor", IsAdminFilter()) +async def handle_servers_editor(callback_query: types.CallbackQuery): + servers = await get_servers_from_db() + + builder = InlineKeyboardBuilder() + + for cluster_name, cluster_servers in servers.items(): + builder.row(InlineKeyboardButton(text=f"⚙️ {cluster_name}", callback_data=f"manage_cluster|{cluster_name}")) + + builder.row(InlineKeyboardButton(text="➕ Добавить кластер", callback_data="add_cluster")) + builder.row(InlineKeyboardButton(text="🔙 Назад в админку", callback_data="admin")) + + await callback_query.message.answer( + "🔧 Управление кластерами\n\n" + "📌 Здесь вы можете добавить новый кластер.\n\n" + "🌐 Кластеры — это пространство серверов, в пределах которого создается подписка.\n" + "💡 Если вы хотите выдавать по 1 серверу, то добавьте всего 1 сервер в кластер.\n\n" + "⚠️ Важно: Кластеры удаляются автоматически, если удалить все серверы внутри них.\n\n", + parse_mode="HTML", + reply_markup=builder.as_markup(), + ) + + +@router.callback_query(F.data == "add_cluster", IsAdminFilter()) +async def handle_add_cluster(callback_query: types.CallbackQuery, state: FSMContext): + await callback_query.message.answer( + "🔧 Введите имя нового кластера:\n\n" + "Имя кластера должно быть уникальным и на английском языке.\n" + "Пример: cluster2 или us_east_1", + parse_mode="HTML", + ) + + await state.set_state(UserEditorState.waiting_for_cluster_name) + + +@router.message(UserEditorState.waiting_for_cluster_name, IsAdminFilter()) +async def handle_cluster_name_input(message: types.Message, state: FSMContext): + cluster_name = message.text.strip() + + if cluster_name == "❌ Отменить": + await state.clear() + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor")) + await message.answer( + "Процесс создания кластера отменен. Вы вернулись в меню управления серверами.", + reply_markup=builder.as_markup(), + ) + return + + if not cluster_name: + await message.answer("❌ Имя кластера не может быть пустым. Попробуйте снова.") + return + + await state.update_data(cluster_name=cluster_name) + + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) + + await message.answer( + f"Введите имя сервера для кластера {cluster_name}:\n\n" + "Рекомендуется указать локацию сервера в имени.\n\n" + "Пример: server-asia, server-europe", + parse_mode="HTML", + reply_markup=builder.as_markup(), + ) + await state.set_state(UserEditorState.waiting_for_server_name) + + +@router.message(UserEditorState.waiting_for_server_name, IsAdminFilter()) +async def handle_server_name_input(message: types.Message, state: FSMContext): + server_name = message.text.strip() + + if server_name == "❌ Отменить": + await state.clear() + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor")) + await message.answer( + "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", + reply_markup=builder.as_markup(), + ) + return + + if not server_name: + await message.answer("❌ Имя сервера не может быть пустым. Попробуйте снова.") + return + + user_data = await state.get_data() + cluster_name = user_data.get('cluster_name') + await state.update_data(server_name=server_name) + + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) + + await message.answer( + f"Введите API URL для сервера {server_name} в кластере {cluster_name}:\n\n" + "API URL должен быть в следующем формате:\n\n" + "https://your_domain:port/panel_path\n\n" + "URL должен быть без слэша на конце!\n", + parse_mode="HTML", + reply_markup=builder.as_markup(), + ) + await state.set_state(UserEditorState.waiting_for_api_url) + + +@router.message(UserEditorState.waiting_for_api_url, IsAdminFilter()) +async def handle_api_url_input(message: types.Message, state: FSMContext): + api_url = message.text.strip() + + if api_url == "❌ Отменить": + await state.clear() + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor")) + await message.answer( + "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", + reply_markup=builder.as_markup(), + ) + return + + if not api_url.startswith("https://"): + await message.answer( + "❌ API URL должен начинаться с https://. Попробуйте снова.", + parse_mode="HTML", + ) + return + + user_data = await state.get_data() + cluster_name = user_data.get('cluster_name') + server_name = user_data.get('server_name') + await state.update_data(api_url=api_url) + + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) + + await message.answer( + f"Введите subscription_url для сервера {server_name} в кластере {cluster_name}:\n\n" + "Subscription URL должен быть в следующем формате:\n\n" + "https://your_domain:port_sub/sub_path\n\n" + "URL должен быть без слэша и имени клиента на конце!\n" + "Его можно увидеть в панели 3x-ui в информации о клиенте.", + parse_mode="HTML", + reply_markup=builder.as_markup(), + ) + await state.set_state(UserEditorState.waiting_for_subscription_url) + + +@router.message(UserEditorState.waiting_for_subscription_url, IsAdminFilter()) +async def handle_subscription_url_input(message: types.Message, state: FSMContext): + subscription_url = message.text.strip() + + if subscription_url == "❌ Отменить": + await state.clear() + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor")) + await message.answer( + "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", + reply_markup=builder.as_markup(), + ) + return + + if not subscription_url.startswith("https://"): + await message.answer( + "❌ subscription_url должен начинаться с https://. Попробуйте снова.", + parse_mode="HTML", + ) + return + + user_data = await state.get_data() + cluster_name = user_data.get('cluster_name') + server_name = user_data.get('server_name') + await state.update_data(subscription_url=subscription_url) + + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) + + await message.answer( + f"Введите inbound_id для сервера {server_name} в кластере {cluster_name}:\n\n" + "Это номер подключения vless в вашей панели 3x-ui. Обычно это 1 при чистой настройке по гайду.\n\n", + parse_mode="HTML", + reply_markup=builder.as_markup(), + ) + await state.set_state(UserEditorState.waiting_for_inbound_id) + + +@router.message(UserEditorState.waiting_for_inbound_id, IsAdminFilter()) +async def handle_inbound_id_input(message: types.Message, state: FSMContext): + inbound_id = message.text.strip() + + if not inbound_id.isdigit(): + await message.answer("❌ inbound_id должен быть числовым значением. Попробуйте снова.") + return + + user_data = await state.get_data() + cluster_name = user_data.get('cluster_name') + server_name = user_data.get('server_name') + api_url = user_data.get('api_url') + subscription_url = user_data.get('subscription_url') + + conn = await asyncpg.connect(DATABASE_URL) + await conn.execute( + """ + INSERT INTO servers (cluster_name, server_name, api_url, subscription_url, inbound_id) + VALUES ($1, $2, $3, $4, $5) + """, + cluster_name, + server_name, + api_url, + subscription_url, + inbound_id, + ) + await conn.close() + + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="🔙 Назад к кластерам", callback_data="servers_editor")) + + await message.answer( + f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!", reply_markup=builder.as_markup() + ) + + await state.clear() + + +@router.callback_query(F.data.startswith("manage_cluster|"), IsAdminFilter()) +async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMContext): + cluster_name = callback_query.data.split("|")[1] + + servers = await get_servers_from_db() + cluster_servers = servers.get(cluster_name, []) + + builder = InlineKeyboardBuilder() + + for server in cluster_servers: + builder.row( + InlineKeyboardButton( + text=f"🌍 {server['server_name']}", callback_data=f"manage_server|{server['server_name']}" + ) + ) + + builder.row(InlineKeyboardButton(text="➕ Добавить сервер", callback_data=f"add_server|{cluster_name}")) + builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor")) + + await callback_query.message.answer( + f"🔧 Управление серверами для кластера {cluster_name}", reply_markup=builder.as_markup() + ) + + +@router.callback_query(F.data.startswith("manage_server|"), IsAdminFilter()) +async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMContext): + server_name = callback_query.data.split("|")[1] + + servers = await get_servers_from_db() + + server = None + cluster_name = None + for cluster, cluster_servers in servers.items(): + server = next((s for s in cluster_servers if s['server_name'] == server_name), None) + if server: + cluster_name = cluster + break + + if server: + api_url = server['api_url'] + subscription_url = server['subscription_url'] + inbound_id = server['inbound_id'] + + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"delete_server|{server_name}")) + builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}")) + + await callback_query.message.answer( + f"🔧 Информация о сервере {server_name}:\n\n" + f"📡 API URL: {api_url}\n" + f"🌐 Subscription URL: {subscription_url}\n" + f"🔑 Inbound ID: {inbound_id}", + parse_mode="HTML", + reply_markup=builder.as_markup(), + ) + else: + await callback_query.message.answer("❌ Сервер не найден.") + + +@router.callback_query(F.data.startswith("delete_server|"), IsAdminFilter()) +async def handle_delete_server(callback_query: types.CallbackQuery, state: FSMContext): + server_name = callback_query.data.split("|")[1] + + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_delete_server|{server_name}"), + InlineKeyboardButton(text="❌ Нет", callback_data=f"manage_server|{server_name}"), + ) + + await callback_query.message.answer( + f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?", reply_markup=builder.as_markup() + ) + + +@router.callback_query(F.data.startswith("confirm_delete_server|"), IsAdminFilter()) +async def handle_confirm_delete_server(callback_query: types.CallbackQuery, state: FSMContext): + server_name = callback_query.data.split("|")[1] + + conn = await asyncpg.connect(DATABASE_URL) + await conn.execute( + """ + DELETE FROM servers WHERE server_name = $1 + """, + server_name, + ) + await conn.close() + + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor")) + + await callback_query.message.answer(f"🗑️ Сервер {server_name} успешно удален.", reply_markup=builder.as_markup()) + + +@router.callback_query(F.data.startswith("add_server|"), IsAdminFilter()) +async def handle_add_server(callback_query: types.CallbackQuery, state: FSMContext): + cluster_name = callback_query.data.split("|")[1] + + await state.update_data(cluster_name=cluster_name) + + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) + + await callback_query.message.answer( + f"Введите имя сервера для кластера {cluster_name}:\n\n" + "Рекомендуется указать локацию сервера в имени.\n\n" + "Пример: server-asia, server-europe", + parse_mode="HTML", + reply_markup=builder.as_markup(), + ) + + await state.set_state(UserEditorState.waiting_for_server_name) diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 38a66aa9..318b2f08 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -9,7 +9,7 @@ from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from config import TOTAL_GB -from database import get_client_id_by_email, restore_trial, update_key_expiry, get_servers_from_db +from database import 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 @@ -397,11 +397,11 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se async def update_key_on_all_servers(): tasks = [] for cluster_name, cluster_servers in clusters.items(): - for server in cluster_servers: + for server in cluster_servers: tasks.append( asyncio.create_task( renew_key_in_cluster( - cluster_name, + cluster_name, email, client_id, expiry_time, @@ -462,7 +462,7 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery, sessi async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any): client_id = callback_query.data.split("|")[1] record = await session.fetchrow("SELECT email FROM keys WHERE client_id = $1", client_id) - + if record: email = record["email"] response_message = "✅ Ключ успешно удален." @@ -489,7 +489,6 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) - @router.callback_query(F.data.startswith("user_info|"), IsAdminFilter()) async def handle_user_info(callback_query: types.CallbackQuery, state: FSMContext, session: Any): tg_id = int(callback_query.data.split("|")[1]) diff --git a/handlers/start.py b/handlers/start.py index c5e240e8..915b8ded 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -113,4 +113,4 @@ async def handle_about_vpn(callback_query: CallbackQuery): ) builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="start")) - await callback_query.message.answer(get_about_vpn("3.2_Stable"), reply_markup=builder.as_markup()) + await callback_query.message.answer(get_about_vpn("3.2.1-beta"), reply_markup=builder.as_markup())