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())