821 lines
37 KiB
Python
821 lines
37 KiB
Python
import asyncio
|
||
|
||
from datetime import datetime, timezone
|
||
from typing import Any
|
||
|
||
from aiogram import F, types
|
||
from aiogram.types import CallbackQuery
|
||
from py3xui import AsyncApi
|
||
from sqlalchemy import delete, select, update
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from config import (
|
||
ADMIN_PASSWORD,
|
||
ADMIN_USERNAME,
|
||
REMNAWAVE_LOGIN,
|
||
REMNAWAVE_PASSWORD,
|
||
USE_COUNTRY_SELECTION,
|
||
)
|
||
from core.bootstrap import MODES_CONFIG
|
||
from database import get_servers
|
||
from database.models import Key, Server, Tariff
|
||
from filters.admin import IsAdminFilter
|
||
from handlers.keys.operations import (
|
||
create_client_on_server,
|
||
create_key_on_cluster,
|
||
delete_key_from_cluster,
|
||
)
|
||
from handlers.keys.operations.aggregated_links import make_aggregated_link
|
||
from handlers.utils import ALLOWED_GROUP_CODES
|
||
from logger import logger
|
||
from panels.remnawave import RemnawaveAPI
|
||
from utils.backup import create_backup_and_send_to_admins
|
||
|
||
from ..panel.keyboard import build_admin_back_kb
|
||
from .base import router
|
||
from .keyboard import AdminClusterCallback, build_availability_kb, build_sync_cluster_kb
|
||
|
||
|
||
@router.callback_query(AdminClusterCallback.filter(F.action == "availability"), IsAdminFilter())
|
||
async def handle_cluster_availability(
|
||
callback_query: types.CallbackQuery,
|
||
callback_data: AdminClusterCallback,
|
||
session: Any,
|
||
):
|
||
cluster_name = callback_data.data
|
||
servers = await get_servers(session)
|
||
cluster_servers = servers.get(cluster_name, [])
|
||
|
||
if not cluster_servers:
|
||
await callback_query.message.edit_text(text=f"Кластер '{cluster_name}' не содержит серверов.")
|
||
return
|
||
|
||
await callback_query.message.edit_text(
|
||
text=(
|
||
f"🖥️ Проверка доступности серверов для кластера {cluster_name}.\n\n"
|
||
"Это может занять до 1 минуты, пожалуйста, подождите..."
|
||
)
|
||
)
|
||
|
||
total_online_users = 0
|
||
result_text = f"<b>🖥️ Проверка доступности серверов</b>\n\n⚙️ Кластер: <b>{cluster_name}</b>\n\n"
|
||
|
||
for server in cluster_servers:
|
||
server_name = server["server_name"]
|
||
panel_type = server.get("panel_type", "3x-ui").lower()
|
||
prefix = "[3x]" if panel_type == "3x-ui" else "[Re]"
|
||
|
||
try:
|
||
if panel_type == "3x-ui":
|
||
xui = AsyncApi(
|
||
server["api_url"],
|
||
username=ADMIN_USERNAME,
|
||
password=ADMIN_PASSWORD,
|
||
logger=None,
|
||
)
|
||
await xui.login()
|
||
inbound_id = int(server["inbound_id"])
|
||
online_clients = await xui.client.online()
|
||
online_inbound_users = 0
|
||
|
||
for client_email in online_clients:
|
||
client = await xui.client.get_by_email(client_email)
|
||
if client and client.inbound_id == inbound_id:
|
||
online_inbound_users += 1
|
||
|
||
total_online_users += online_inbound_users
|
||
result_text += f"🌍 <b>{prefix} {server_name}</b> - {online_inbound_users} онлайн\n"
|
||
|
||
elif panel_type == "remnawave":
|
||
server_inbound_id = server.get("inbound_id")
|
||
if not server_inbound_id:
|
||
raise Exception("Не указан inbound_id сервера")
|
||
|
||
remna = RemnawaveAPI(server["api_url"])
|
||
nodes_data = await remna.get_all_nodes_with_online(
|
||
username=REMNAWAVE_LOGIN,
|
||
password=REMNAWAVE_PASSWORD,
|
||
inbound_id=server_inbound_id,
|
||
)
|
||
|
||
if nodes_data.get("error"):
|
||
raise Exception(nodes_data["error"])
|
||
|
||
online_remna_users = nodes_data["total_online"]
|
||
total_online_users += online_remna_users
|
||
|
||
nodes_info = nodes_data["nodes"]
|
||
result_text += f"🌍 <b>{prefix} {server_name}</b> - {online_remna_users} онлайн\n"
|
||
seen = set()
|
||
for node_info in nodes_info:
|
||
node_name = node_info.get("name", "Unknown")
|
||
if node_name in seen:
|
||
continue
|
||
seen.add(node_name)
|
||
|
||
country_code = node_info.get("country_code", "Unknown")
|
||
online_users = node_info.get("online_users", 0)
|
||
|
||
flag = (
|
||
"".join(chr(ord(c) + 127397) for c in country_code.upper())
|
||
if country_code != "Unknown" and len(country_code) == 2
|
||
else country_code
|
||
)
|
||
result_text += f" ↳ {flag} ({node_name}): {online_users} онлайн\n"
|
||
|
||
except Exception as e:
|
||
error_text = str(e) or "Сервер недоступен"
|
||
result_text += f"❌ <b>{prefix} {server_name}</b> - ошибка: {error_text}\n"
|
||
|
||
result_text += f"\n👥 Всего пользователей онлайн: {total_online_users}"
|
||
await callback_query.message.edit_text(
|
||
text=result_text,
|
||
reply_markup=build_availability_kb(cluster_name),
|
||
)
|
||
|
||
|
||
@router.callback_query(AdminClusterCallback.filter(F.action == "backup"), IsAdminFilter())
|
||
async def handle_clusters_backup(
|
||
callback_query: types.CallbackQuery,
|
||
callback_data: AdminClusterCallback,
|
||
session: Any,
|
||
):
|
||
cluster_name = callback_data.data
|
||
|
||
servers = await get_servers(session)
|
||
cluster_servers = servers.get(cluster_name, [])
|
||
|
||
for server in cluster_servers:
|
||
if server.get("panel_type") == "remnawave":
|
||
continue
|
||
|
||
xui = AsyncApi(
|
||
server["api_url"],
|
||
username=ADMIN_USERNAME,
|
||
password=ADMIN_PASSWORD,
|
||
logger=logger,
|
||
)
|
||
await create_backup_and_send_to_admins(xui)
|
||
|
||
text = (
|
||
f"<b>Бэкап для кластера {cluster_name} был успешно создан и отправлен администраторам!</b>\n\n"
|
||
f"🔔 <i>Бэкапы отправлены в боты панелей (3x-ui).</i>"
|
||
)
|
||
|
||
await callback_query.message.edit_text(
|
||
text=text,
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
|
||
|
||
@router.callback_query(AdminClusterCallback.filter(F.action == "sync"), IsAdminFilter())
|
||
async def handle_sync(
|
||
callback_query: types.CallbackQuery,
|
||
callback_data: AdminClusterCallback,
|
||
session: Any,
|
||
):
|
||
cluster_name = callback_data.data
|
||
|
||
servers = await get_servers(session)
|
||
cluster_servers = servers.get(cluster_name, [])
|
||
|
||
await callback_query.message.edit_text(
|
||
text=f"<b>🔄 Синхронизация кластера {cluster_name}</b>",
|
||
reply_markup=build_sync_cluster_kb(cluster_servers, cluster_name),
|
||
)
|
||
|
||
|
||
@router.callback_query(AdminClusterCallback.filter(F.action == "sync-server"), IsAdminFilter())
|
||
async def handle_sync_server(
|
||
callback_query: types.CallbackQuery,
|
||
callback_data: AdminClusterCallback,
|
||
session: AsyncSession,
|
||
):
|
||
server_name = callback_data.data
|
||
|
||
try:
|
||
server_result = await session.execute(
|
||
select(Server.cluster_name).where(Server.server_name == server_name).limit(1)
|
||
)
|
||
cluster_name = server_result.scalar()
|
||
|
||
if not cluster_name:
|
||
await callback_query.message.edit_text(
|
||
text=f"❌ Сервер {server_name} не найден.",
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
return
|
||
|
||
use_country_selection = bool(MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION))
|
||
|
||
if use_country_selection:
|
||
stmt = (
|
||
select(
|
||
Server.api_url,
|
||
Server.inbound_id,
|
||
Server.server_name,
|
||
Server.panel_type,
|
||
Key.tg_id,
|
||
Key.client_id,
|
||
Key.email,
|
||
Key.expiry_time,
|
||
Key.tariff_id,
|
||
Key.remnawave_link,
|
||
Key.selected_device_limit,
|
||
Key.selected_traffic_limit,
|
||
Key.current_device_limit,
|
||
Key.current_traffic_limit,
|
||
)
|
||
.join(Key, Server.server_name == Key.server_id)
|
||
.where(Server.server_name == server_name)
|
||
)
|
||
else:
|
||
stmt = (
|
||
select(
|
||
Server.api_url,
|
||
Server.inbound_id,
|
||
Server.server_name,
|
||
Server.panel_type,
|
||
Key.tg_id,
|
||
Key.client_id,
|
||
Key.email,
|
||
Key.expiry_time,
|
||
Key.tariff_id,
|
||
Key.remnawave_link,
|
||
Key.selected_device_limit,
|
||
Key.selected_traffic_limit,
|
||
Key.current_device_limit,
|
||
Key.current_traffic_limit,
|
||
)
|
||
.join(Key, Server.cluster_name == Key.server_id)
|
||
.where(Server.server_name == server_name)
|
||
)
|
||
|
||
result = await session.execute(stmt)
|
||
keys_to_sync = result.mappings().all()
|
||
|
||
if not keys_to_sync:
|
||
await callback_query.message.edit_text(
|
||
text=f"❌ Нет ключей для синхронизации в сервере {server_name}.",
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
return
|
||
|
||
await callback_query.message.edit_text(
|
||
text=f"<b>🔄 Синхронизация сервера {server_name}</b>\n\n🔑 Количество ключей: <b>{len(keys_to_sync)}</b>"
|
||
)
|
||
|
||
all_servers = await get_servers(session)
|
||
cluster_servers = all_servers.get(cluster_name, [])
|
||
|
||
server_info = None
|
||
for s in cluster_servers:
|
||
if s.get("server_name") == server_name:
|
||
server_info = s
|
||
break
|
||
|
||
tariff_ids = {key["tariff_id"] for key in keys_to_sync if key["tariff_id"]}
|
||
tariffs_cache = {}
|
||
if tariff_ids:
|
||
tariffs_result = await session.execute(select(Tariff).where(Tariff.id.in_(tariff_ids)))
|
||
tariffs_list = tariffs_result.scalars().all()
|
||
tariffs_cache = {t.id: dict(t.__dict__) for t in tariffs_list}
|
||
|
||
semaphore = asyncio.Semaphore(2)
|
||
for key in keys_to_sync:
|
||
try:
|
||
if key["panel_type"] == "remnawave":
|
||
tariff = tariffs_cache.get(key["tariff_id"]) if key["tariff_id"] else None
|
||
|
||
if tariff and server_info:
|
||
subgroup = tariff.get("subgroup_title")
|
||
tid = key["tariff_id"]
|
||
has_new_binding = tid and tid in (server_info.get("tariff_ids") or [])
|
||
has_old_binding = subgroup and subgroup in (server_info.get("tariff_subgroups") or [])
|
||
has_any_binding = bool(server_info.get("tariff_ids") or server_info.get("tariff_subgroups"))
|
||
|
||
if has_any_binding and subgroup and not has_new_binding and not has_old_binding:
|
||
continue
|
||
|
||
if tariff.get("group_code") and tariff.get("group_code").lower() in ALLOWED_GROUP_CODES:
|
||
if tariff.get("group_code").lower() not in server_info.get("special_groups", []):
|
||
continue
|
||
|
||
expire_iso = (
|
||
datetime.utcfromtimestamp(key["expiry_time"] / 1000).replace(tzinfo=timezone.utc).isoformat()
|
||
)
|
||
|
||
remna = RemnawaveAPI(key["api_url"])
|
||
if not await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
|
||
logger.error(f"Не удалось авторизоваться в Remnawave для сервера {server_name}")
|
||
continue
|
||
|
||
traffic_limit_bytes = 0
|
||
hwid_limit = 0
|
||
|
||
current_device_limit_from_key = key.get("current_device_limit")
|
||
current_traffic_limit_gb_from_key = key.get("current_traffic_limit")
|
||
selected_device_limit_from_key = key.get("selected_device_limit")
|
||
selected_traffic_limit_gb_from_key = key.get("selected_traffic_limit")
|
||
|
||
if tariff:
|
||
if current_traffic_limit_gb_from_key is not None:
|
||
traffic_limit_bytes = int(current_traffic_limit_gb_from_key * 1024**3)
|
||
elif selected_traffic_limit_gb_from_key is not None:
|
||
traffic_limit_bytes = int(selected_traffic_limit_gb_from_key * 1024**3)
|
||
elif tariff.get("traffic_limit") is not None:
|
||
traffic_limit_bytes = int(tariff.get("traffic_limit") * 1024**3)
|
||
|
||
if current_device_limit_from_key is not None:
|
||
hwid_limit = int(current_device_limit_from_key)
|
||
elif selected_device_limit_from_key is not None:
|
||
hwid_limit = int(selected_device_limit_from_key)
|
||
else:
|
||
hwid_limit = tariff.get("device_limit")
|
||
|
||
success = await remna.update_user(
|
||
uuid=key["client_id"],
|
||
expire_at=expire_iso,
|
||
telegram_id=key["tg_id"],
|
||
email=f"{key['email']}@fake.local",
|
||
active_user_inbounds=[key["inbound_id"]],
|
||
traffic_limit_bytes=traffic_limit_bytes,
|
||
hwid_device_limit=hwid_limit,
|
||
)
|
||
|
||
if success:
|
||
try:
|
||
sub = await remna.get_subscription_by_username(key["email"])
|
||
if sub:
|
||
new_remnawave_link = sub.get("subscriptionUrl")
|
||
|
||
if new_remnawave_link:
|
||
key_value = await make_aggregated_link(
|
||
session=session,
|
||
cluster_all=cluster_servers,
|
||
cluster_id=cluster_name,
|
||
email=key["email"],
|
||
client_id=key["client_id"],
|
||
tg_id=key["tg_id"],
|
||
remna_link_override=None,
|
||
plan=tariff,
|
||
)
|
||
|
||
await session.execute(
|
||
update(Key)
|
||
.where(Key.tg_id == key["tg_id"], Key.client_id == key["client_id"])
|
||
.values(remnawave_link=new_remnawave_link, key=key_value)
|
||
)
|
||
await session.commit()
|
||
logger.info(f"[Sync] Обновлена ссылка для {key['email']}: {new_remnawave_link}")
|
||
except Exception as e:
|
||
logger.warning(f"[Sync] Не удалось получить ссылку для {key['email']}: {e}")
|
||
|
||
if not success:
|
||
logger.warning("[Sync] ошибка обновления, пробуем пересоздать")
|
||
|
||
await delete_key_from_cluster(server_name, key["email"], key["client_id"], session)
|
||
|
||
await create_key_on_cluster(
|
||
cluster_id=server_name,
|
||
tg_id=key["tg_id"],
|
||
client_id=key["client_id"],
|
||
email=key["email"],
|
||
expiry_timestamp=key["expiry_time"],
|
||
plan=key["tariff_id"],
|
||
session=session,
|
||
remnawave_link=key["remnawave_link"],
|
||
hwid_limit=hwid_limit,
|
||
traffic_limit_bytes=traffic_limit_bytes,
|
||
selected_device_limit=key.get("selected_device_limit"),
|
||
selected_traffic_limit_gb=key.get("selected_traffic_limit"),
|
||
current_device_limit=key.get("current_device_limit"),
|
||
current_traffic_limit_gb=key.get("current_traffic_limit"),
|
||
selected_price_rub=key.get("selected_price_rub"),
|
||
)
|
||
else:
|
||
await create_client_on_server(
|
||
{
|
||
"api_url": key["api_url"],
|
||
"inbound_id": key["inbound_id"],
|
||
"server_name": key["server_name"],
|
||
},
|
||
key["tg_id"],
|
||
key["client_id"],
|
||
key["email"],
|
||
key["expiry_time"],
|
||
semaphore,
|
||
plan=key["tariff_id"],
|
||
session=session,
|
||
)
|
||
await asyncio.sleep(0.6)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при синхронизации ключа {key['client_id']} в сервер {server_name}: {e}")
|
||
|
||
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.edit_text(
|
||
text=f"❌ Произошла ошибка при синхронизации: {e}",
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
|
||
|
||
@router.callback_query(AdminClusterCallback.filter(F.action == "sync-cluster"), IsAdminFilter())
|
||
async def handle_sync_cluster(
|
||
callback_query: CallbackQuery,
|
||
callback_data: AdminClusterCallback,
|
||
session: AsyncSession,
|
||
):
|
||
cluster_name = callback_data.data
|
||
|
||
try:
|
||
servers = await get_servers(session)
|
||
cluster_servers = servers.get(cluster_name, [])
|
||
|
||
use_country_selection = bool(MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION))
|
||
|
||
if use_country_selection:
|
||
server_names = [s.get("server_name") for s in cluster_servers if s.get("server_name")]
|
||
if not server_names:
|
||
await callback_query.message.edit_text(
|
||
text=f"❌ В кластере {cluster_name} нет серверов.",
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
return
|
||
result = await session.execute(
|
||
select(
|
||
Key.tg_id,
|
||
Key.client_id,
|
||
Key.email,
|
||
Key.expiry_time,
|
||
Key.remnawave_link,
|
||
Key.tariff_id,
|
||
Key.server_id,
|
||
Key.selected_device_limit,
|
||
Key.selected_traffic_limit,
|
||
Key.current_device_limit,
|
||
Key.current_traffic_limit,
|
||
).where(Key.server_id.in_(server_names), Key.is_frozen.is_(False))
|
||
)
|
||
else:
|
||
result = await session.execute(
|
||
select(
|
||
Key.tg_id,
|
||
Key.client_id,
|
||
Key.email,
|
||
Key.expiry_time,
|
||
Key.remnawave_link,
|
||
Key.tariff_id,
|
||
Key.server_id,
|
||
Key.selected_device_limit,
|
||
Key.selected_traffic_limit,
|
||
Key.current_device_limit,
|
||
Key.current_traffic_limit,
|
||
).where(Key.server_id == cluster_name, Key.is_frozen.is_(False))
|
||
)
|
||
|
||
keys_to_sync = result.mappings().all()
|
||
|
||
if not keys_to_sync:
|
||
await callback_query.message.edit_text(
|
||
text=f"❌ Нет ключей для синхронизации в кластере {cluster_name}.",
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
return
|
||
only_remnawave = all(s.get("panel_type") == "remnawave" for s in cluster_servers)
|
||
|
||
await callback_query.message.edit_text(
|
||
text=f"<b>🔄 Синхронизация кластера {cluster_name}</b>\n\n🔑 Количество ключей: <b>{len(keys_to_sync)}</b>"
|
||
)
|
||
|
||
tariff_ids = {key["tariff_id"] for key in keys_to_sync if key["tariff_id"]}
|
||
tariffs_cache = {}
|
||
if tariff_ids:
|
||
tariffs_result = await session.execute(select(Tariff).where(Tariff.id.in_(tariff_ids)))
|
||
tariffs_list = tariffs_result.scalars().all()
|
||
tariffs_cache = {t.id: dict(t.__dict__) for t in tariffs_list}
|
||
|
||
if only_remnawave:
|
||
batch_size = 50
|
||
total_keys = len(keys_to_sync)
|
||
processed_count = 0
|
||
|
||
for batch_start in range(0, total_keys, batch_size):
|
||
batch = keys_to_sync[batch_start : batch_start + batch_size]
|
||
batch_end = batch_start + len(batch)
|
||
logger.info(f"[Sync] Обработка батча {batch_start}-{batch_end} из {total_keys}")
|
||
|
||
async def update_remnawave_api(key):
|
||
try:
|
||
traffic_limit_bytes = 0
|
||
hwid_limit = 0
|
||
subgroup_title = None
|
||
tariff = tariffs_cache.get(key["tariff_id"]) if key["tariff_id"] else None
|
||
|
||
current_device_limit_from_key = key.get("current_device_limit")
|
||
current_traffic_limit_gb_from_key = key.get("current_traffic_limit")
|
||
selected_device_limit_from_key = key.get("selected_device_limit")
|
||
selected_traffic_limit_gb_from_key = key.get("selected_traffic_limit")
|
||
|
||
if tariff:
|
||
if current_traffic_limit_gb_from_key is not None:
|
||
traffic_limit_bytes = int(current_traffic_limit_gb_from_key * 1024**3)
|
||
elif selected_traffic_limit_gb_from_key is not None:
|
||
traffic_limit_bytes = int(selected_traffic_limit_gb_from_key * 1024**3)
|
||
elif tariff.get("traffic_limit") is not None:
|
||
traffic_limit_bytes = int(tariff.get("traffic_limit") * 1024**3)
|
||
else:
|
||
traffic_limit_bytes = 0
|
||
|
||
if current_device_limit_from_key is not None:
|
||
hwid_limit = int(current_device_limit_from_key)
|
||
elif selected_device_limit_from_key is not None:
|
||
hwid_limit = int(selected_device_limit_from_key)
|
||
else:
|
||
hwid_limit = tariff.get("device_limit")
|
||
|
||
subgroup_title = tariff.get("subgroup_title")
|
||
|
||
expire_iso = (
|
||
datetime.utcfromtimestamp(key["expiry_time"] / 1000)
|
||
.replace(tzinfo=timezone.utc)
|
||
.isoformat()
|
||
)
|
||
|
||
if use_country_selection:
|
||
user_server = None
|
||
for s in cluster_servers:
|
||
if s.get("server_name") == key["server_id"]:
|
||
user_server = s
|
||
break
|
||
|
||
if not user_server:
|
||
return {"key": key, "success": False, "error": "Server not found"}
|
||
|
||
remna = RemnawaveAPI(user_server["api_url"])
|
||
inbound_ids = [user_server["inbound_id"]] if user_server.get("inbound_id") else []
|
||
else:
|
||
remna = RemnawaveAPI(cluster_servers[0]["api_url"])
|
||
|
||
filtered_servers = cluster_servers
|
||
if subgroup_title or (tariff and tariff.get("id")):
|
||
tid = tariff.get("id") if tariff else None
|
||
filtered_servers = [
|
||
s
|
||
for s in cluster_servers
|
||
if (tid and tid in (s.get("tariff_ids") or []))
|
||
or (subgroup_title and subgroup_title in (s.get("tariff_subgroups") or []))
|
||
]
|
||
if not filtered_servers:
|
||
filtered_servers = cluster_servers
|
||
|
||
if tariff and tariff.get("group_code"):
|
||
group_code = tariff.get("group_code").lower()
|
||
if group_code in ALLOWED_GROUP_CODES:
|
||
special_filtered = [
|
||
s for s in filtered_servers if group_code in (s.get("special_groups") or [])
|
||
]
|
||
if special_filtered:
|
||
filtered_servers = special_filtered
|
||
|
||
inbound_ids = [s["inbound_id"] for s in filtered_servers if s.get("inbound_id")]
|
||
|
||
if not await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
|
||
return {"key": key, "success": False, "error": "Login failed"}
|
||
|
||
success = await remna.update_user(
|
||
uuid=key["client_id"],
|
||
expire_at=expire_iso,
|
||
telegram_id=key["tg_id"],
|
||
email=f"{key['email']}@fake.local",
|
||
active_user_inbounds=inbound_ids,
|
||
traffic_limit_bytes=traffic_limit_bytes,
|
||
hwid_device_limit=hwid_limit,
|
||
)
|
||
|
||
if success:
|
||
sub = await remna.get_subscription_by_username(key["email"])
|
||
new_link = sub.get("subscriptionUrl") if sub else None
|
||
return {
|
||
"key": key,
|
||
"success": True,
|
||
"new_link": new_link,
|
||
"tariff": tariff,
|
||
"traffic_limit_bytes": traffic_limit_bytes,
|
||
"hwid_limit": hwid_limit,
|
||
}
|
||
else:
|
||
return {
|
||
"key": key,
|
||
"success": False,
|
||
"needs_recreate": True,
|
||
"tariff": tariff,
|
||
"traffic_limit_bytes": traffic_limit_bytes,
|
||
"hwid_limit": hwid_limit,
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"[Sync] Ошибка API для {key.get('email')}: {e}")
|
||
return {"key": key, "success": False, "error": str(e)}
|
||
|
||
tasks = [update_remnawave_api(key) for key in batch]
|
||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||
|
||
bulk_updates = []
|
||
recreate_tasks = []
|
||
|
||
for result in results:
|
||
if isinstance(result, Exception):
|
||
logger.error(f"[Sync] Exception в батче: {result}")
|
||
continue
|
||
|
||
if not isinstance(result, dict):
|
||
continue
|
||
|
||
key = result.get("key")
|
||
if not key:
|
||
continue
|
||
|
||
try:
|
||
if result.get("success") and result.get("new_link"):
|
||
new_link = result["new_link"]
|
||
tariff = result.get("tariff")
|
||
|
||
key_value = await make_aggregated_link(
|
||
session=session,
|
||
cluster_all=cluster_servers,
|
||
cluster_id=cluster_name,
|
||
email=key["email"],
|
||
client_id=key["client_id"],
|
||
tg_id=key["tg_id"],
|
||
remna_link_override=None,
|
||
plan=tariff,
|
||
)
|
||
|
||
bulk_updates.append({
|
||
"client_id": key["client_id"],
|
||
"remnawave_link": new_link,
|
||
"key": key_value,
|
||
})
|
||
|
||
elif result.get("needs_recreate"):
|
||
recreate_tasks.append((key, result))
|
||
|
||
except Exception as e:
|
||
logger.error(f"[Sync] Ошибка подготовки для {key.get('email')}: {e}")
|
||
|
||
if bulk_updates:
|
||
try:
|
||
await session.run_sync(
|
||
lambda sync_session: sync_session.bulk_update_mappings(Key, bulk_updates)
|
||
)
|
||
await session.commit()
|
||
logger.info(f"[Sync] Bulk: обновлено {len(bulk_updates)} ключей")
|
||
except Exception as bulk_error:
|
||
logger.warning(f"[Sync] Bulk упал, fallback: {bulk_error}")
|
||
await session.rollback()
|
||
|
||
for upd in bulk_updates:
|
||
try:
|
||
await session.execute(
|
||
update(Key)
|
||
.where(Key.client_id == upd["client_id"])
|
||
.values(remnawave_link=upd["remnawave_link"], key=upd["key"])
|
||
)
|
||
await session.commit()
|
||
except Exception as e:
|
||
logger.error(f"[Sync] Fallback ошибка {upd['client_id']}: {e}")
|
||
await session.rollback()
|
||
|
||
for key, result in recreate_tasks:
|
||
try:
|
||
logger.warning(f"[Sync] Пересоздание {key['email']}")
|
||
await delete_key_from_cluster(cluster_name, key["email"], key["client_id"], session)
|
||
await session.execute(
|
||
delete(Key).where(Key.tg_id == key["tg_id"], Key.client_id == key["client_id"])
|
||
)
|
||
await session.commit()
|
||
|
||
cluster_id_for_recreate = key["server_id"] if use_country_selection else cluster_name
|
||
await create_key_on_cluster(
|
||
cluster_id_for_recreate,
|
||
key["tg_id"],
|
||
key["client_id"],
|
||
key["email"],
|
||
key["expiry_time"],
|
||
plan=key["tariff_id"],
|
||
session=session,
|
||
remnawave_link=key["remnawave_link"],
|
||
hwid_limit=result.get("hwid_limit"),
|
||
traffic_limit_bytes=result.get("traffic_limit_bytes"),
|
||
selected_device_limit=key.get("selected_device_limit"),
|
||
selected_traffic_limit_gb=key.get("selected_traffic_limit"),
|
||
current_device_limit=key.get("current_device_limit"),
|
||
current_traffic_limit_gb=key.get("current_traffic_limit"),
|
||
selected_price_rub=key.get("selected_price_rub"),
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"[Sync] Пересоздание ошибка {key.get('email')}: {e}")
|
||
|
||
processed_count = batch_end
|
||
progress_percent = int((processed_count / total_keys) * 100)
|
||
progress_bar = "█" * (progress_percent // 5) + "░" * (20 - progress_percent // 5)
|
||
|
||
try:
|
||
await callback_query.message.edit_text(
|
||
text=(
|
||
f"<b>🔄 Синхронизация кластера {cluster_name}</b>\n\n"
|
||
f"🔑 Количество ключей: <b>{total_keys}</b>\n\n"
|
||
f"Обработано: <b>{processed_count}/{total_keys}</b>\n"
|
||
f"<code>{progress_bar}</code>"
|
||
)
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
else:
|
||
for key in keys_to_sync:
|
||
try:
|
||
traffic_limit_bytes = 0
|
||
hwid_limit = 0
|
||
tariff = tariffs_cache.get(key["tariff_id"]) if key["tariff_id"] else None
|
||
|
||
current_device_limit_from_key = key.get("current_device_limit")
|
||
current_traffic_limit_gb_from_key = key.get("current_traffic_limit")
|
||
selected_device_limit_from_key = key.get("selected_device_limit")
|
||
selected_traffic_limit_gb_from_key = key.get("selected_traffic_limit")
|
||
|
||
if tariff:
|
||
if current_traffic_limit_gb_from_key is not None:
|
||
traffic_limit_bytes = int(current_traffic_limit_gb_from_key * 1024**3)
|
||
elif selected_traffic_limit_gb_from_key is not None:
|
||
traffic_limit_bytes = int(selected_traffic_limit_gb_from_key * 1024**3)
|
||
elif tariff.get("traffic_limit") is not None:
|
||
traffic_limit_bytes = int(tariff.get("traffic_limit") * 1024**3)
|
||
else:
|
||
traffic_limit_bytes = 0
|
||
|
||
if current_device_limit_from_key is not None:
|
||
hwid_limit = int(current_device_limit_from_key)
|
||
elif selected_device_limit_from_key is not None:
|
||
hwid_limit = int(selected_device_limit_from_key)
|
||
else:
|
||
hwid_limit = tariff.get("device_limit")
|
||
|
||
tariff.get("subgroup_title")
|
||
elif key["tariff_id"]:
|
||
logger.warning(
|
||
f"[Sync] Ключ {key['client_id']} с несуществующим тарифом ID={key['tariff_id']} — "
|
||
f"обновим без лимитов"
|
||
)
|
||
|
||
await delete_key_from_cluster(cluster_name, key["email"], key["client_id"], session)
|
||
|
||
await session.execute(
|
||
delete(Key).where(Key.tg_id == key["tg_id"], Key.client_id == key["client_id"])
|
||
)
|
||
|
||
cluster_id_for_recreate = key["server_id"] if use_country_selection else cluster_name
|
||
await create_key_on_cluster(
|
||
cluster_id_for_recreate,
|
||
key["tg_id"],
|
||
key["client_id"],
|
||
key["email"],
|
||
key["expiry_time"],
|
||
plan=key["tariff_id"],
|
||
session=session,
|
||
remnawave_link=key["remnawave_link"],
|
||
hwid_limit=hwid_limit,
|
||
traffic_limit_bytes=traffic_limit_bytes,
|
||
selected_device_limit=key.get("selected_device_limit"),
|
||
selected_traffic_limit_gb=key.get("selected_traffic_limit"),
|
||
current_device_limit=key.get("current_device_limit"),
|
||
current_traffic_limit_gb=key.get("current_traffic_limit"),
|
||
selected_price_rub=key.get("selected_price_rub"),
|
||
)
|
||
|
||
await asyncio.sleep(0.5)
|
||
|
||
except Exception as e:
|
||
logger.error(f"[Sync] Ошибка при обработке ключа {key['client_id']} в {cluster_name}: {e}")
|
||
|
||
await callback_query.message.edit_text(
|
||
text=(
|
||
f"✅ <b>Синхронизация завершена</b>\n\n"
|
||
f"📊 Кластер: <b>{cluster_name}</b>\n"
|
||
f"🔑 Обработано ключей: <b>{len(keys_to_sync)}</b>"
|
||
),
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"[Sync] Ошибка синхронизации кластера {cluster_name}: {e}")
|
||
await callback_query.message.edit_text(
|
||
text=f"❌ Произошла ошибка при синхронизации: {e}",
|
||
reply_markup=build_admin_back_kb("clusters"),
|
||
)
|