diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py
index 1c0e8ed0..ccfed1ae 100644
--- a/handlers/admin/clusters/clusters_handler.py
+++ b/handlers/admin/clusters/clusters_handler.py
@@ -9,18 +9,21 @@ from aiogram.types import CallbackQuery, Message
from py3xui import AsyncApi
from backup import create_backup_and_send_to_admins
-from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
-from database import check_unique_server_name, get_servers
+from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, TOTAL_GB
+from database import check_unique_server_name, get_servers, update_key_expiry
from filters.admin import IsAdminFilter
-from handlers.keys.key_utils import create_key_on_cluster, create_client_on_server
+from handlers.keys.key_utils import create_key_on_cluster, create_client_on_server, renew_key_in_cluster
from logger import logger
from .keyboard import (
build_clusters_editor_kb,
build_manage_cluster_kb,
AdminClusterCallback,
+ AdminServerCallback,
build_sync_cluster_kb,
+ build_cluster_management_kb
)
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
+import time
router = Router()
@@ -31,6 +34,7 @@ class AdminClusterStates(StatesGroup):
waiting_for_inbound_id = State()
waiting_for_server_name = State()
waiting_for_subscription_url = State()
+ waiting_for_days_input = State()
@router.callback_query(
@@ -78,7 +82,7 @@ async def handle_cluster_name_input(message: Message, state: FSMContext):
if len(message.text) > 12:
await message.answer(
- text="❌ Имя кластера должно превышать 12 символов! Попробуйте снова.",
+ text="❌ Имя кластера не должно превышать 12 символов! Попробуйте снова.",
reply_markup=build_admin_back_kb("clusters"),
)
return
@@ -332,7 +336,7 @@ async def handle_sync(callback_query: types.CallbackQuery, callback_data: AdminC
servers = await get_servers(session)
cluster_servers = servers.get(cluster_name, [])
- await callback_query.message.answer(
+ await callback_query.message.edit_text(
text=f"🔄 Синхронизация кластера {cluster_name}",
reply_markup=build_sync_cluster_kb(cluster_servers, cluster_name),
)
@@ -352,7 +356,7 @@ async def handle_sync_server(callback_query: types.CallbackQuery, callback_data:
keys_to_sync = await session.fetch(query_keys, server_name)
if not keys_to_sync:
- await callback_query.message.answer(
+ await callback_query.message.edit_text(
text=f"❌ Нет ключей для синхронизации в сервере {server_name}.",
reply_markup=build_admin_back_kb("clusters"),
)
@@ -360,7 +364,7 @@ async def handle_sync_server(callback_query: types.CallbackQuery, callback_data:
text = f"🔄 Синхронизация сервера {server_name}\n\n🔑 Количество ключей: {len(keys_to_sync)}"
- await callback_query.message.answer(
+ await callback_query.message.edit_text(
text=text,
)
@@ -383,13 +387,13 @@ async def handle_sync_server(callback_query: types.CallbackQuery, callback_data:
except Exception as e:
logger.error(f"Ошибка при добавлении ключа {key['client_id']} в сервер {server_name}: {e}")
- await callback_query.message.answer(
+ await callback_query.message.edit_text(
text=f"✅ Ключи успешно синхронизированы для сервера {server_name}",
reply_markup=build_admin_back_kb("clusters"),
)
except Exception as e:
logger.error(f"Ошибка синхронизации ключей для сервера {server_name}: {e}")
- await callback_query.message.answer(
+ await callback_query.message.edit_text(
text=f"❌ Произошла ошибка при синхронизации: {e}", reply_markup=build_admin_back_kb("clusters")
)
@@ -407,7 +411,7 @@ async def handle_sync_cluster(callback_query: types.CallbackQuery, callback_data
keys_to_sync = await session.fetch(query_keys, cluster_name)
if not keys_to_sync:
- await callback_query.message.answer(
+ await callback_query.message.edit_text(
text=f"❌ Нет ключей для синхронизации в кластере {cluster_name}.",
reply_markup=build_admin_back_kb("clusters"),
)
@@ -415,7 +419,7 @@ async def handle_sync_cluster(callback_query: types.CallbackQuery, callback_data
text = f"🔄 Синхронизация кластера {cluster_name}\n\n🔑 Количество ключей: {len(keys_to_sync)}"
- await callback_query.message.answer(
+ await callback_query.message.edit_text(
text=text,
)
@@ -432,12 +436,99 @@ async def handle_sync_cluster(callback_query: types.CallbackQuery, callback_data
except Exception as e:
logger.error(f"Ошибка при добавлении ключа {key['client_id']} в кластер {cluster_name}: {e}")
- await callback_query.message.answer(
+ await callback_query.message.edit_text(
text=f"✅ Ключи успешно синхронизированы для кластера {cluster_name}",
reply_markup=build_admin_back_kb("clusters"),
)
except Exception as e:
logger.error(f"Ошибка синхронизации ключей в кластере {cluster_name}: {e}")
- await callback_query.message.answer(
+ await callback_query.message.edit_text(
text=f"❌ Произошла ошибка при синхронизации: {e}", reply_markup=build_admin_back_kb("clusters")
)
+
+
+@router.callback_query(AdminServerCallback.filter(F.action == "add"), IsAdminFilter())
+async def handle_add_server(callback_query: CallbackQuery, callback_data: AdminServerCallback, state: FSMContext):
+ cluster_name = callback_data.data
+
+ await state.update_data(cluster_name=cluster_name)
+
+ text = (
+ f"Введите имя сервера для кластера {cluster_name}:\n\n"
+ "Рекомендуется указать локацию и номер сервера в имени.\n\n"
+ "Пример: de1, fra1, fi2"
+ )
+
+ await callback_query.message.edit_text(
+ text=text,
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+
+ await state.set_state(AdminClusterStates.waiting_for_server_name)
+
+
+@router.callback_query(AdminClusterCallback.filter(F.action == "manage_cluster"), IsAdminFilter())
+async def handle_manage_cluster_menu(callback_query: CallbackQuery, callback_data: AdminClusterCallback):
+ cluster_name = callback_data.data
+
+ await callback_query.message.edit_text(
+ text=f"🛠 Управление кластером {cluster_name}\nВыберите действие:",
+ reply_markup=build_cluster_management_kb(cluster_name),
+ )
+
+
+@router.callback_query(AdminClusterCallback.filter(F.action == "add_time"), IsAdminFilter())
+async def handle_add_time(callback_query: CallbackQuery, callback_data: AdminClusterCallback, state: FSMContext):
+ cluster_name = callback_data.data
+ await state.set_state(AdminClusterStates.waiting_for_days_input)
+ await state.update_data(cluster_name=cluster_name)
+
+ await callback_query.message.edit_text(
+ f"⏳ Введите количество дней, на которое хотите продлить все подписки в кластере {cluster_name}:",
+ reply_markup=build_admin_back_kb(f"manage_cluster|{cluster_name}")
+ )
+
+
+@router.message(AdminClusterStates.waiting_for_days_input, IsAdminFilter())
+async def handle_days_input(message: Message, state: FSMContext, session: Any):
+ try:
+ days = int(message.text.strip())
+ if days <= 0:
+ raise ValueError
+
+ user_data = await state.get_data()
+ cluster_name = user_data.get("cluster_name")
+
+ now = int(time.time() * 1000)
+ add_ms = days * 86400 * 1000
+
+ keys = await session.fetch(
+ "SELECT tg_id, client_id, email, expiry_time FROM keys WHERE server_id = $1",
+ cluster_name,
+ )
+
+ if not keys:
+ await message.answer("❌ Нет подписок в этом кластере.")
+ await state.clear()
+ return
+
+ for key in keys:
+ new_expiry = (key["expiry_time"] or now) + add_ms
+ await renew_key_in_cluster(
+ cluster_name,
+ email=key["email"],
+ client_id=key["client_id"],
+ new_expiry_time=new_expiry,
+ total_gb=TOTAL_GB,
+ )
+ await update_key_expiry(key["client_id"], new_expiry, session)
+
+ await message.answer(f"✅ Время подписки продлено на {days} дней всем пользователям в кластере {cluster_name}.")
+ except ValueError:
+ await message.answer("❌ Введите корректное число дней.")
+ return
+ except Exception as e:
+ logger.error(f"Ошибка при добавлении дней: {e}")
+ await message.answer("❌ Произошла ошибка при продлении времени.")
+ finally:
+ await state.clear()
diff --git a/handlers/admin/clusters/keyboard.py b/handlers/admin/clusters/keyboard.py
index ff4731dd..e233b4dd 100644
--- a/handlers/admin/clusters/keyboard.py
+++ b/handlers/admin/clusters/keyboard.py
@@ -53,25 +53,51 @@ def build_manage_cluster_kb(cluster_servers: list, cluster_name: str) -> InlineK
)
)
+ builder.row(
+ InlineKeyboardButton(
+ text="🛠 Управление",
+ callback_data=AdminClusterCallback(action="manage_cluster", data=cluster_name).pack(),
+ )
+ )
+
+ builder.row(build_admin_back_btn("clusters"))
+ return builder.as_markup()
+
+
+def build_cluster_management_kb(cluster_name: str) -> InlineKeyboardMarkup:
+ builder = InlineKeyboardBuilder()
+
builder.row(
InlineKeyboardButton(
text="🌐 Доступность",
callback_data=AdminClusterCallback(action="availability", data=cluster_name).pack(),
- ),
+ )
+ )
+ builder.row(
InlineKeyboardButton(
text="🔄 Синхронизация",
callback_data=AdminClusterCallback(action="sync", data=cluster_name).pack(),
- ),
+ )
)
-
builder.row(
InlineKeyboardButton(
text="💾 Создать бэкап",
callback_data=AdminClusterCallback(action="backup", data=cluster_name).pack(),
)
)
+ builder.row(
+ InlineKeyboardButton(
+ text="⏳ Добавить время",
+ callback_data=AdminClusterCallback(action="add_time", data=cluster_name).pack(),
+ )
+ )
+ builder.row(
+ InlineKeyboardButton(
+ text="🔙 Назад",
+ callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack(),
+ )
+ )
- builder.row(build_admin_back_btn("clusters"))
return builder.as_markup()