455 lines
17 KiB
Python
455 lines
17 KiB
Python
from datetime import datetime
|
||
|
||
from aiogram import F, types
|
||
from aiogram.fsm.context import FSMContext
|
||
from aiogram.types import CallbackQuery, Message
|
||
from sqlalchemy import func, select, update
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from database import get_servers, update_key_expiry
|
||
from database.models import Key, Server, Tariff
|
||
from filters.admin import IsAdminFilter
|
||
from handlers.keys.operations import renew_key_in_cluster
|
||
from logger import logger
|
||
|
||
from ..panel.keyboard import build_admin_back_kb
|
||
from .base import AdminClusterStates, router
|
||
from .keyboard import (
|
||
AdminClusterCallback,
|
||
AdminServerCallback,
|
||
build_cluster_management_kb,
|
||
build_manage_cluster_kb,
|
||
)
|
||
|
||
|
||
@router.callback_query(AdminClusterCallback.filter(F.action == "manage"), IsAdminFilter())
|
||
async def handle_clusters_manage(
|
||
callback_query: types.CallbackQuery,
|
||
callback_data: AdminClusterCallback,
|
||
session: AsyncSession,
|
||
):
|
||
cluster_name = callback_data.data
|
||
|
||
result = await session.execute(
|
||
select(Server.tariff_group)
|
||
.where(
|
||
Server.cluster_name == cluster_name,
|
||
Server.tariff_group.isnot(None),
|
||
)
|
||
.limit(1)
|
||
)
|
||
row = result.first()
|
||
tariff_group = row[0] if row else "—"
|
||
|
||
result = await session.execute(select(Server.server_name).where(Server.cluster_name == cluster_name))
|
||
server_names = [row[0] for row in result.all()]
|
||
result = await session.execute(
|
||
select(func.count(func.distinct(Key.tg_id))).where(
|
||
(Key.server_id == cluster_name) | (Key.server_id.in_(server_names))
|
||
)
|
||
)
|
||
user_count = result.scalar() or 0
|
||
|
||
result = await session.execute(
|
||
select(func.count()).where((Key.server_id == cluster_name) | (Key.server_id.in_(server_names)))
|
||
)
|
||
subscription_count = result.scalar() or 0
|
||
|
||
text = (
|
||
f"<b>🔧 Управление кластером <code>{cluster_name}</code></b>\n\n"
|
||
f"📁 <b>Тарифная группа:</b> <code>{tariff_group}</code>\n"
|
||
f"👥 <b>Пользователей на кластере:</b> <code>{user_count}</code>\n"
|
||
f"🔑 <b>Всего подписок:</b> <code>{subscription_count}</code>"
|
||
)
|
||
|
||
await callback_query.message.edit_text(
|
||
text=text,
|
||
reply_markup=build_cluster_management_kb(cluster_name),
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data.startswith("cluster_servers|"), IsAdminFilter())
|
||
async def handle_cluster_servers(callback: CallbackQuery, session: AsyncSession):
|
||
cluster_name = callback.data.split("|", 1)[1]
|
||
servers = await get_servers(session=session, include_enabled=True)
|
||
cluster_servers = servers.get(cluster_name, [])
|
||
|
||
from handlers.utils import ALLOWED_GROUP_CODES
|
||
|
||
allowed = set(ALLOWED_GROUP_CODES)
|
||
lines = []
|
||
for s in cluster_servers:
|
||
tids = s.get("tariff_ids") or []
|
||
subs = s.get("tariff_subgroups") or []
|
||
if tids:
|
||
subs_str = f"{len(tids)} тариф(ов)"
|
||
elif subs:
|
||
subs_str = ", ".join(sorted(subs))
|
||
else:
|
||
subs_str = "—"
|
||
|
||
grps = s.get("special_groups") or []
|
||
grps = [g for g in grps if g in allowed]
|
||
grps_str = ", ".join(sorted(grps)) if grps else "—"
|
||
|
||
lines.append(f"• {s.get('server_name', '?')} — {subs_str} | {grps_str}")
|
||
|
||
details = "\n".join(lines) if lines else "нет серверов"
|
||
|
||
await callback.message.edit_text(
|
||
text=(
|
||
f"<b>📡 Серверы в кластере {cluster_name}</b>\n<i>подгруппы | спецгруппы:</i>\n"
|
||
f"<blockquote>{details}</blockquote>"
|
||
),
|
||
reply_markup=build_manage_cluster_kb(cluster_servers, 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"⏳ Введите количество дней, на которое хотите продлить все подписки в кластере <b>{cluster_name}</b>:",
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
|
||
|
||
@router.message(AdminClusterStates.waiting_for_days_input, IsAdminFilter())
|
||
async def handle_days_input(message: Message, state: FSMContext, session: AsyncSession):
|
||
try:
|
||
days = int(message.text.strip())
|
||
if days <= 0:
|
||
raise ValueError
|
||
|
||
user_data = await state.get_data()
|
||
cluster_name = user_data.get("cluster_name")
|
||
add_ms = days * 86400 * 1000
|
||
|
||
logger.info(f"[Cluster Extend] Добавляем {days} дней для кластера: {cluster_name}")
|
||
|
||
server_stmt = select(Server.server_name).where(Server.cluster_name == cluster_name)
|
||
server_rows = await session.execute(server_stmt)
|
||
server_names = [row[0] for row in server_rows.all()]
|
||
server_names.append(cluster_name)
|
||
|
||
result = await session.execute(select(Key).where(Key.server_id.in_(server_names)))
|
||
keys = result.scalars().all()
|
||
|
||
if not keys:
|
||
await message.answer("❌ Нет подписок в этом кластере или сервере.")
|
||
await state.clear()
|
||
return
|
||
|
||
servers = await get_servers(session=session)
|
||
cluster_servers = servers.get(cluster_name, [])
|
||
|
||
if not cluster_servers:
|
||
await message.answer("❌ Не найдены серверы в кластере.")
|
||
await state.clear()
|
||
return
|
||
|
||
is_full_remnawave = all(
|
||
str(s.get("panel_type", "")).lower() == "remnawave"
|
||
for s in cluster_servers
|
||
)
|
||
|
||
if is_full_remnawave:
|
||
uuids = [key.client_id for key in keys if key.client_id]
|
||
|
||
if not uuids:
|
||
await message.answer("❌ Нет валидных подписок для продления.")
|
||
await state.clear()
|
||
return
|
||
|
||
api_url = cluster_servers[0].get("api_url", "")
|
||
if not api_url:
|
||
await message.answer("❌ Не найден URL панели для кластера.")
|
||
await state.clear()
|
||
return
|
||
|
||
from panels.remnawave import RemnawaveAPI
|
||
remna = RemnawaveAPI(api_url)
|
||
|
||
try:
|
||
result_bulk = await remna.bulk_extend_expiration_date(uuids, days)
|
||
finally:
|
||
await remna.aclose()
|
||
|
||
if result_bulk is None:
|
||
await message.answer("❌ Ошибка при обращении к API панели.")
|
||
await state.clear()
|
||
return
|
||
|
||
affected = result_bulk.get("affectedRows", 0)
|
||
logger.info(f"[Cluster Extend] Bulk API: продлено {affected} подписок на {days} дней")
|
||
|
||
for key in keys:
|
||
new_expiry = key.expiry_time + add_ms
|
||
await update_key_expiry(session, key.client_id, new_expiry)
|
||
|
||
await session.commit()
|
||
|
||
await message.answer(
|
||
f"✅ Время подписки продлено на <b>{days} дней</b> для <b>{affected}</b> пользователей в кластере <b>{cluster_name}</b>."
|
||
)
|
||
else:
|
||
for key in keys:
|
||
new_expiry = key.expiry_time + add_ms
|
||
|
||
traffic_limit = 0
|
||
device_limit = 0
|
||
key_subgroup = None
|
||
if key.tariff_id:
|
||
tariff_result = await session.execute(
|
||
select(Tariff.traffic_limit, Tariff.device_limit, Tariff.subgroup_title).where(
|
||
Tariff.id == key.tariff_id,
|
||
Tariff.is_active.is_(True),
|
||
)
|
||
)
|
||
tariff = tariff_result.first()
|
||
if tariff:
|
||
traffic_limit = int(tariff[0]) if tariff[0] is not None else 0
|
||
device_limit = int(tariff[1]) if tariff[1] is not None else 0
|
||
key_subgroup = tariff[2]
|
||
|
||
await renew_key_in_cluster(
|
||
cluster_name,
|
||
email=key.email,
|
||
client_id=key.client_id,
|
||
new_expiry_time=new_expiry,
|
||
total_gb=traffic_limit,
|
||
session=session,
|
||
hwid_device_limit=device_limit,
|
||
reset_traffic=False,
|
||
target_subgroup=key_subgroup,
|
||
old_subgroup=key_subgroup,
|
||
plan=key.tariff_id,
|
||
)
|
||
await update_key_expiry(session, key.client_id, new_expiry)
|
||
|
||
logger.info(f"[Cluster Extend] {key.email} +{days}д → {datetime.utcfromtimestamp(new_expiry / 1000)}")
|
||
|
||
await message.answer(
|
||
f"✅ Время подписки продлено на <b>{days} дней</b> всем пользователям в кластере <b>{cluster_name}</b>."
|
||
)
|
||
|
||
except ValueError:
|
||
await message.answer("❌ Введите корректное число дней.")
|
||
except Exception as e:
|
||
logger.error(f"[Cluster Extend] Ошибка при добавлении дней: {e}")
|
||
await message.answer("❌ Произошла ошибка при продлении времени.")
|
||
finally:
|
||
await state.clear()
|
||
|
||
|
||
@router.callback_query(AdminClusterCallback.filter(F.action == "rename"), IsAdminFilter())
|
||
async def handle_rename_cluster(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminClusterCallback,
|
||
state: FSMContext,
|
||
):
|
||
cluster_name = callback_data.data
|
||
await state.update_data(old_cluster_name=cluster_name)
|
||
|
||
text = (
|
||
f"✏️ <b>Введите новое имя для кластера '{cluster_name}':</b>\n\n"
|
||
"▸ Имя должно быть уникальным.\n"
|
||
"▸ Имя не должно превышать 12 символов.\n\n"
|
||
"📌 <i>Пример:</i> <code>new_cluster</code>"
|
||
)
|
||
|
||
await callback_query.message.edit_text(
|
||
text=text,
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
await state.set_state(AdminClusterStates.waiting_for_new_cluster_name)
|
||
|
||
|
||
@router.message(AdminClusterStates.waiting_for_new_cluster_name, IsAdminFilter())
|
||
async def handle_new_cluster_name_input(message: Message, state: FSMContext, session: AsyncSession):
|
||
if not message.text:
|
||
await message.answer(
|
||
text="❌ Имя кластера не может быть пустым! Попробуйте снова.",
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
return
|
||
|
||
new_cluster_name = message.text.strip()
|
||
if len(new_cluster_name) > 12:
|
||
await message.answer(
|
||
text="❌ Имя кластера не должно превышать 12 символов! Попробуйте снова.",
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
return
|
||
|
||
user_data = await state.get_data()
|
||
old_cluster_name = user_data.get("old_cluster_name")
|
||
|
||
try:
|
||
result = await session.execute(
|
||
select(Server.cluster_name).where(Server.cluster_name == new_cluster_name).limit(1)
|
||
)
|
||
existing_cluster = result.scalar()
|
||
|
||
if existing_cluster:
|
||
await message.answer(
|
||
text=f"❌ Кластер с именем '{new_cluster_name}' уже существует. Введите другое имя.",
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
return
|
||
|
||
keys_count_result = await session.execute(
|
||
select(func.count()).select_from(Key).where(Key.server_id == old_cluster_name)
|
||
)
|
||
keys_count = keys_count_result.scalar()
|
||
|
||
await session.execute(
|
||
update(Server).where(Server.cluster_name == old_cluster_name).values(cluster_name=new_cluster_name)
|
||
)
|
||
|
||
if keys_count > 0:
|
||
await session.execute(
|
||
update(Key).where(Key.server_id == old_cluster_name).values(server_id=new_cluster_name)
|
||
)
|
||
|
||
await session.commit()
|
||
|
||
await message.answer(
|
||
text=f"✅ Название кластера успешно изменено с '{old_cluster_name}' на '{new_cluster_name}'!",
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
except Exception as e:
|
||
await session.rollback()
|
||
logger.error(f"Ошибка при смене имени кластера {old_cluster_name} на {new_cluster_name}: {e}")
|
||
await message.answer(
|
||
text=f"❌ Произошла ошибка при смене имени кластера: {e}",
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
finally:
|
||
await state.clear()
|
||
|
||
|
||
@router.callback_query(AdminServerCallback.filter(F.action == "rename"), IsAdminFilter())
|
||
async def handle_rename_server(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminServerCallback,
|
||
state: FSMContext,
|
||
session: AsyncSession,
|
||
):
|
||
old_server_name = callback_data.data
|
||
|
||
servers = await get_servers(session=session)
|
||
cluster_name = None
|
||
for c_name, server_list in servers.items():
|
||
for server in server_list:
|
||
if server["server_name"] == old_server_name:
|
||
cluster_name = c_name
|
||
break
|
||
if cluster_name:
|
||
break
|
||
|
||
if not cluster_name:
|
||
await callback_query.message.edit_text(
|
||
text=f"❌ Не удалось найти кластер для сервера '{old_server_name}'.",
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
return
|
||
|
||
await state.update_data(old_server_name=old_server_name, cluster_name=cluster_name)
|
||
|
||
text = (
|
||
f"✏️ <b>Введите новое имя для сервера '{old_server_name}' в кластере '{cluster_name}':</b>\n\n"
|
||
"▸ Имя должно быть уникальным в пределах кластера.\n"
|
||
"▸ Имя не должно превышать 12 символов.\n\n"
|
||
"📌 <i>Пример:</i> <code>new_server</code>"
|
||
)
|
||
|
||
await callback_query.message.edit_text(
|
||
text=text,
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
await state.set_state(AdminClusterStates.waiting_for_new_server_name)
|
||
|
||
|
||
@router.message(AdminClusterStates.waiting_for_new_server_name, IsAdminFilter())
|
||
async def handle_new_server_name_input(message: Message, state: FSMContext, session: AsyncSession):
|
||
if not message.text:
|
||
await message.answer(
|
||
text="❌ Имя сервера не может быть пустым! Попробуйте снова.",
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
return
|
||
|
||
new_server_name = message.text.strip()
|
||
if len(new_server_name) > 12:
|
||
await message.answer(
|
||
text="❌ Имя сервера не должно превышать 12 символов! Попробуйте снова.",
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
return
|
||
|
||
user_data = await state.get_data()
|
||
old_server_name = user_data.get("old_server_name")
|
||
cluster_name = user_data.get("cluster_name")
|
||
|
||
try:
|
||
result = await session.execute(
|
||
select(Server)
|
||
.where(
|
||
Server.cluster_name == cluster_name,
|
||
Server.server_name == new_server_name,
|
||
)
|
||
.limit(1)
|
||
)
|
||
existing_server = result.scalar()
|
||
if existing_server:
|
||
await message.answer(
|
||
text=(
|
||
f"❌ Сервер с именем '{new_server_name}' уже существует в кластере '{cluster_name}'. "
|
||
f"Введите другое имя."
|
||
),
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
return
|
||
|
||
result = await session.execute(select(func.count()).select_from(Key).where(Key.server_id == old_server_name))
|
||
keys_count = result.scalar()
|
||
|
||
await session.execute(
|
||
update(Server)
|
||
.where(
|
||
Server.cluster_name == cluster_name,
|
||
Server.server_name == old_server_name,
|
||
)
|
||
.values(server_name=new_server_name)
|
||
)
|
||
|
||
if keys_count > 0:
|
||
await session.execute(update(Key).where(Key.server_id == old_server_name).values(server_id=new_server_name))
|
||
|
||
await session.commit()
|
||
|
||
await message.answer(
|
||
text=(
|
||
f"✅ Название сервера успешно изменено с '{old_server_name}' на '{new_server_name}' "
|
||
f"в кластере '{cluster_name}'!"
|
||
),
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
except Exception as e:
|
||
await session.rollback()
|
||
logger.error(f"Ошибка при смене имени сервера {old_server_name} на {new_server_name}: {e}")
|
||
await message.answer(
|
||
text=f"❌ Произошла ошибка при смене имени сервера: {e}",
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
finally:
|
||
await state.clear()
|