Files
Solo_bot/handlers/keys/key_utils.py
T
2025-05-30 22:12:16 +03:00

1050 lines
40 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import asyncio
from datetime import datetime, timezone
from typing import Any
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from config import PUBLIC_LINK, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, SUPERNODE, TRIAL_CONFIG
from database import delete_notification, get_servers, get_tariff_by_id, store_key
from database.models import Key, Server, Tariff
from handlers.utils import check_server_key_limit, get_least_loaded_cluster
from logger import logger
from panels.remnawave import RemnawaveAPI
from panels.three_xui import (
ClientConfig,
add_client,
delete_client,
extend_client_key,
get_client_traffic,
get_xui_instance,
toggle_client,
)
async def create_key_on_cluster(
cluster_id: str,
tg_id: int,
client_id: str,
email: str,
expiry_timestamp: int,
plan: int = None,
session: AsyncSession = None,
remnawave_link: str = None,
hwid_limit: int = None,
traffic_limit_bytes: int = None,
is_trial: bool = False,
):
try:
servers = await get_servers(session, include_enabled=True)
cluster = servers.get(cluster_id)
server_id_to_store = cluster_id
if not cluster:
found_servers = []
for _key, server_list in servers.items():
for server_info in server_list:
if server_info.get("server_name", "").lower() == cluster_id.lower():
found_servers.append(server_info)
if found_servers:
cluster = found_servers
server_id_to_store = found_servers[0].get("server_name")
else:
raise ValueError(
f"Кластер или сервер с ID/именем {cluster_id} не найден."
)
enabled_servers = [s for s in cluster if s.get("enabled", True)]
if not enabled_servers:
logger.warning(
f"[Key Creation] Нет доступных серверов в кластере {cluster_id}"
)
return
if plan is not None and traffic_limit_bytes is None:
tariff = await get_tariff_by_id(session, plan)
if not tariff:
raise ValueError(f"Тариф с id={plan} не найден.")
traffic_limit_bytes = (
int(tariff["traffic_limit"]) if tariff["traffic_limit"] else None
)
if hwid_limit is None and tariff.get("device_limit") is not None:
hwid_limit = int(tariff["device_limit"])
remnawave_servers = [
s
for s in enabled_servers
if s.get("panel_type", "3x-ui").lower() == "remnawave"
and await check_server_key_limit(s, session)
]
xui_servers = [
s
for s in enabled_servers
if s.get("panel_type", "3x-ui").lower() == "3x-ui"
and await check_server_key_limit(s, session)
]
if not remnawave_servers and not xui_servers:
logger.warning(
f"[Key Creation] Нет серверов с доступным лимитом в кластере {cluster_id}"
)
return
semaphore = asyncio.Semaphore(2)
remnawave_created = False
remnawave_key = None
remnawave_client_id = None
if remnawave_servers:
remna = RemnawaveAPI(remnawave_servers[0]["api_url"])
logged_in = await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD)
if not logged_in:
logger.error("Не удалось войти в Remnawave API")
else:
expire_at = (
datetime.utcfromtimestamp(expiry_timestamp / 1000).isoformat() + "Z"
)
inbound_ids = [
s.get("inbound_id")
for s in remnawave_servers
if s.get("inbound_id")
]
if not inbound_ids:
logger.warning("Нет inbound_id у серверов Remnawave")
else:
short_uuid = None
if remnawave_link and "/" in remnawave_link:
short_uuid = remnawave_link.rstrip("/").split("/")[-1]
user_data = {
"username": email,
"trafficLimitStrategy": "NO_RESET",
"expireAt": expire_at,
"telegramId": tg_id,
"activeUserInbounds": inbound_ids,
}
if traffic_limit_bytes and traffic_limit_bytes > 0:
user_data["trafficLimitBytes"] = traffic_limit_bytes * 1024 * 1024 * 1024
if short_uuid:
user_data["shortUuid"] = short_uuid
if hwid_limit is not None:
user_data["hwidDeviceLimit"] = hwid_limit
result = await remna.create_user(user_data)
if not result:
logger.error("Ошибка при создании пользователя в Remnawave")
else:
remnawave_created = True
remnawave_key = result.get("subscriptionUrl")
remnawave_client_id = result.get("uuid")
logger.info(
f"[Key Creation] Пользователь создан в Remnawave: {result}"
)
public_link = f"{PUBLIC_LINK}{email}/{tg_id}" if xui_servers else None
final_client_id = remnawave_client_id or client_id
logger.info(
f"[Debug] 3x-ui servers для кластера {cluster_id}: {[s['server_name'] for s in xui_servers]}"
)
if xui_servers:
if SUPERNODE:
for server_info in xui_servers:
await create_client_on_server(
server_info,
tg_id,
final_client_id,
email,
expiry_timestamp,
semaphore,
plan=plan,
session=session,
is_trial=is_trial,
)
else:
await asyncio.gather(
*[
create_client_on_server(
server,
tg_id,
final_client_id,
email,
expiry_timestamp,
semaphore,
plan=plan,
session=session,
is_trial=is_trial,
)
for server in xui_servers
],
return_exceptions=True,
)
if (remnawave_created and remnawave_client_id) or xui_servers:
await store_key(
session=session,
tg_id=tg_id,
client_id=final_client_id,
email=email,
expiry_time=expiry_timestamp,
key=public_link,
server_id=server_id_to_store,
remnawave_link=remnawave_key,
tariff_id=plan,
)
except Exception as e:
logger.error(f"Ошибка при создании ключа: {e}")
raise e
async def create_client_on_server(
server_info: dict,
tg_id: int,
client_id: str,
email: str,
expiry_timestamp: int,
semaphore: asyncio.Semaphore,
plan: int = None,
session=None,
is_trial: bool = False,
):
"""
Создает клиента на указанном 3x-ui сервере с лимитом по тарифу или триалу.
"""
logger.info(
f"[Client] Вход в create_client_on_server: сервер={server_info.get('server_name')}, план={plan}, is_trial={is_trial}"
)
async with semaphore:
xui = await get_xui_instance(server_info["api_url"])
inbound_id = server_info.get("inbound_id")
server_name = server_info.get("server_name", "unknown")
if not inbound_id:
logger.warning(f"[Client] INBOUND_ID отсутствует для сервера {server_name}. Пропуск.")
return
if SUPERNODE:
unique_email = f"{email}_{server_name.lower()}"
sub_id = email
else:
unique_email = email
sub_id = unique_email
total_gb_value = 0
device_limit_value = None
if is_trial:
total_gb_value = TRIAL_CONFIG.get("traffic_limit_gb", 0)
device_limit_value = TRIAL_CONFIG.get("hwid_limit")
logger.info(f"[Trial] Используются параметры триала: {total_gb_value} GB, {device_limit_value} устройств")
elif plan is not None:
tariff = await get_tariff_by_id(session, plan)
logger.info(f"[Tariff Debug] Получен тариф: {tariff}")
if not tariff:
raise ValueError(f"Тариф с id={plan} не найден.")
total_gb_value = int(tariff["traffic_limit"]) if tariff["traffic_limit"] else 0
device_limit_value = int(tariff["device_limit"]) if tariff.get("device_limit") is not None else None
try:
logger.info(
f"[Client] Вызов add_client: email={email}, client_id={client_id}, GB={total_gb_value}, Devices={device_limit_value}"
)
traffic_limit_bytes = total_gb_value * 1024 * 1024 * 1024
await add_client(
xui,
ClientConfig(
client_id=client_id,
email=unique_email,
tg_id=tg_id,
limit_ip=device_limit_value,
total_gb=traffic_limit_bytes,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(inbound_id),
sub_id=sub_id,
),
)
logger.info(f"[Client] Клиент успешно добавлен на сервер {server_name}")
except Exception as e:
logger.error(f"[Client Error] Не удалось создать клиента на {server_name}: {e}")
if SUPERNODE:
await asyncio.sleep(0.7)
async def renew_key_in_cluster(
cluster_id: str,
email: str,
client_id: str,
new_expiry_time: int,
total_gb: int,
session: AsyncSession,
hwid_device_limit: int = None,
):
try:
servers = await get_servers(session)
cluster = servers.get(cluster_id)
if not cluster:
found_servers = []
for _key, server_list in servers.items():
for server_info in server_list:
if server_info.get("server_name", "").lower() == cluster_id.lower():
found_servers.append(server_info)
if found_servers:
cluster = found_servers
else:
raise ValueError(
f"Кластер или сервер с ID/именем {cluster_id} не найден."
)
result = await session.execute(
select(Key.tg_id, Key.server_id).where(Key.client_id == client_id).limit(1)
)
row = result.first()
if not row:
logger.error(
f"Не найден пользователь с client_id={client_id} в таблице keys."
)
return False
tg_id, server_id = row
result = await session.execute(
select(Server.tariff_group).where(Server.server_name == server_id)
)
tariff_group_row = result.scalar_one_or_none()
if tariff_group_row:
result = await session.execute(
select(Tariff)
.where(Tariff.group_code == tariff_group_row, Tariff.is_active is True)
.order_by(Tariff.duration_days.desc())
.limit(1)
)
tariff = result.scalar_one_or_none()
if tariff and tariff.device_limit is not None:
hwid_device_limit = int(tariff.device_limit)
remnawave_inbound_ids = []
tasks = []
for server_info in cluster:
if server_info.get("panel_type", "3x-ui").lower() == "remnawave":
inbound_id = server_info.get("inbound_id")
if inbound_id:
remnawave_inbound_ids.append(inbound_id)
if remnawave_inbound_ids:
remnawave_server = next(
(
s
for s in cluster
if s.get("panel_type", "").lower() == "remnawave"
and s.get("inbound_id") in remnawave_inbound_ids
),
None,
)
if remnawave_server:
remna = RemnawaveAPI(remnawave_server["api_url"])
if await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
expire_iso = (
datetime.utcfromtimestamp(new_expiry_time // 1000).isoformat()
+ "Z"
)
traffic_limit_bytes = total_gb * 1024 * 1024 * 1024 if total_gb else 0
updated = await remna.update_user(
uuid=client_id,
expire_at=expire_iso,
active_user_inbounds=remnawave_inbound_ids,
traffic_limit_bytes=traffic_limit_bytes,
hwid_device_limit=hwid_device_limit,
)
if updated:
logger.info(f"Подписка Remnawave {client_id} успешно продлена")
await remna.reset_user_traffic(client_id)
else:
logger.warning(
f"Не удалось продлить подписку Remnawave {client_id}"
)
else:
logger.error("Не удалось войти в Remnawave API")
for server_info in cluster:
if server_info.get("panel_type", "3x-ui").lower() != "3x-ui":
continue
xui = await get_xui_instance(server_info["api_url"])
inbound_id = server_info.get("inbound_id")
server_name = server_info.get("server_name", "unknown")
if not inbound_id:
logger.warning(
f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск."
)
continue
if SUPERNODE:
unique_email = f"{email}_{server_name.lower()}"
sub_id = email
else:
unique_email = email
sub_id = unique_email
traffic_bytes = total_gb * 1024 * 1024 * 1024 if total_gb else 0
tasks.append(
extend_client_key(
xui=xui,
inbound_id=int(inbound_id),
email=unique_email,
new_expiry_time=new_expiry_time,
client_id=client_id,
total_gb=traffic_bytes,
sub_id=sub_id,
tg_id=tg_id,
limit_ip=hwid_device_limit,
)
)
await asyncio.gather(*tasks, return_exceptions=True)
notification_prefixes = ["key_24h", "key_10h", "key_expired", "renew"]
for notif in notification_prefixes:
notification_id = f"{email}_{notif}"
await delete_notification(session, tg_id, notification_id)
logger.info(f"🧹 Уведомления для ключа {email} очищены при продлении.")
except Exception as e:
logger.error(
f"Не удалось продлить ключ {client_id} в кластере/на сервере {cluster_id}: {e}"
)
raise
async def delete_key_from_cluster(
cluster_id: str, email: str, client_id: str, session: AsyncSession
):
"""Удаление ключа с серверов в кластере или с конкретного сервера"""
try:
servers = await get_servers(session)
cluster = servers.get(cluster_id)
if not cluster:
found_servers = []
for _, server_list in servers.items():
for server_info in server_list:
if server_info.get("server_name", "").lower() == cluster_id.lower():
found_servers.append(server_info)
if found_servers:
cluster = found_servers
else:
raise ValueError(
f"Кластер или сервер с ID/именем {cluster_id} не найден."
)
for server_info in cluster:
panel_type = server_info.get("panel_type", "3x-ui").lower()
server_name = server_info.get("server_name", "unknown")
if panel_type == "remnawave":
remna = RemnawaveAPI(server_info["api_url"])
if not await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
logger.error(
f"[Remnawave] Не удалось войти на сервер {server_name}"
)
continue
success = await remna.delete_user(client_id)
if success:
logger.info(
f"[Remnawave] Клиент {client_id} успешно удалён с {server_name}"
)
else:
logger.warning(
f"[Remnawave] Не удалось удалить клиента {client_id} с {server_name}"
)
elif panel_type == "3x-ui":
xui = await get_xui_instance(server_info["api_url"])
inbound_id = server_info.get("inbound_id")
if not inbound_id:
logger.warning(
f"[3x-ui] INBOUND_ID отсутствует на сервере {server_name}. Пропуск."
)
continue
await delete_client(
xui,
inbound_id=int(inbound_id),
email=email,
client_id=client_id,
)
logger.info(
f"[3x-ui] Клиент {client_id} удалён с сервера {server_name}"
)
else:
logger.warning(
f"[Unknown] Неизвестный тип панели '{panel_type}' для сервера {server_name}"
)
except Exception as e:
logger.error(
f"❌ Ошибка при удалении ключа {client_id} из кластера/сервера {cluster_id}: {e}"
)
raise
async def update_key_on_cluster(
tg_id: int,
client_id: str,
email: str,
expiry_time: int,
cluster_id: str,
session: AsyncSession,
traffic_limit: int = None,
device_limit: int = None,
remnawave_link: str = None,
):
"""
Пересоздаёт ключ на всех серверах указанного кластера (или сервера, если передано имя).
Работает с панелями 3x-ui и Remnawave. Возвращает кортеж: (новый client_id, remnawave ссылка или None).
"""
try:
servers = await get_servers(session)
cluster = servers.get(cluster_id)
if not cluster:
found_servers = []
for _key, server_list in servers.items():
for server_info in server_list:
if server_info.get("server_name", "").lower() == cluster_id.lower():
found_servers.append(server_info)
if found_servers:
cluster = found_servers
else:
raise ValueError(
f"Кластер или сервер с ID/именем {cluster_id} не найден."
)
expire_iso = (
datetime.utcfromtimestamp(expiry_time / 1000)
.replace(tzinfo=timezone.utc)
.isoformat()
)
remnawave_servers = [
s for s in cluster if s.get("panel_type", "3x-ui").lower() == "remnawave"
]
xui_servers = [
s for s in cluster if s.get("panel_type", "3x-ui").lower() == "3x-ui"
]
remnawave_client_id = None
remnawave_key = None
if remnawave_servers:
inbound_ids = [
s["inbound_id"] for s in remnawave_servers if s.get("inbound_id")
]
remna = RemnawaveAPI(remnawave_servers[0]["api_url"])
if await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
await remna.delete_user(client_id)
group_code = remnawave_servers[0].get("tariff_group")
if not group_code:
raise ValueError("У Remnawave-сервера отсутствует tariff_group")
result = await session.execute(
select(Tariff)
.where(Tariff.group_code == group_code, Tariff.is_active is True)
.order_by(Tariff.duration_days.desc())
.limit(1)
)
tariff = result.scalar_one_or_none()
short_uuid = None
if remnawave_link and "/" in remnawave_link:
short_uuid = remnawave_link.rstrip("/").split("/")[-1]
logger.info(f"[Update] Извлечен short_uuid из ссылки: {short_uuid}")
user_data = {
"username": email,
"trafficLimitStrategy": "NO_RESET",
"expireAt": expire_iso,
"telegramId": tg_id,
"activeUserInbounds": inbound_ids,
}
if traffic_limit is not None:
user_data["trafficLimitBytes"] = traffic_limit * 1024 ** 3
if device_limit is not None:
user_data["hwidDeviceLimit"] = device_limit
if short_uuid:
user_data["shortUuid"] = short_uuid
logger.info(f"[Update] Добавлен short_uuid в user_data: {short_uuid}")
result = await remna.create_user(user_data)
if result:
remnawave_client_id = result.get("uuid")
remnawave_key = result.get("subscriptionUrl")
logger.info(
f"[Update] Remnawave: клиент заново создан, новый UUID: {remnawave_client_id}"
)
else:
logger.error("[Update] Ошибка создания Remnawave клиента")
else:
logger.error("[Update] Не удалось авторизоваться в Remnawave")
if not remnawave_client_id:
logger.warning(
f"[Update] Remnawave client_id не получен. Используется исходный: {client_id}"
)
remnawave_client_id = client_id
tasks = []
for server_info in xui_servers:
server_name = server_info.get("server_name", "unknown")
inbound_id = server_info.get("inbound_id")
if not inbound_id:
logger.warning(
f"[Update] INBOUND_ID отсутствует для сервера {server_name}. Пропуск."
)
continue
xui = await get_xui_instance(server_info["api_url"])
sub_id = email
unique_email = f"{email}_{server_name.lower()}" if SUPERNODE else email
group_code = server_info.get("tariff_group")
if not group_code:
raise ValueError(f"У сервера {server_name} отсутствует tariff_group")
result = await session.execute(
select(Tariff)
.where(Tariff.group_code == group_code, Tariff.is_active is True)
.order_by(Tariff.duration_days.desc())
.limit(1)
)
tariff = result.scalar_one_or_none()
total_gb_bytes = int(traffic_limit * 1024 ** 3) if traffic_limit else 0
device_limit_value = device_limit if device_limit is not None else None
config = ClientConfig(
client_id=remnawave_client_id,
email=unique_email,
tg_id=tg_id,
limit_ip=device_limit_value,
total_gb=total_gb_bytes,
expiry_time=expiry_time,
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(inbound_id),
sub_id=sub_id,
)
tasks.append(add_client(xui, config))
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
logger.info(
f"[Update] Ключ {remnawave_client_id} обновлён на всех серверах кластера {cluster_id}"
)
return remnawave_client_id, remnawave_key
except Exception as e:
logger.error(
f"[Update Error] Ошибка при обновлении ключа {client_id} на {cluster_id}: {e}"
)
raise
async def update_subscription(
tg_id: int,
email: str,
session: AsyncSession,
cluster_override: str = None,
country_override: str = None,
remnawave_link: str = None,
) -> None:
result = await session.execute(
select(Key).where(Key.tg_id == tg_id, Key.email == email)
)
record = result.scalar_one_or_none()
if not record:
raise ValueError(f"The key {email} does not exist in database")
expiry_time = record.expiry_time
client_id = record.client_id
old_cluster_id = record.server_id
tariff_id = record.tariff_id
remnawave_link = remnawave_link or record.remnawave_link
public_link = f"{PUBLIC_LINK}{email}/{tg_id}"
traffic_limit = None
device_limit = None
if tariff_id:
result = await session.execute(
select(Tariff).where(Tariff.id == tariff_id, Tariff.is_active.is_(True))
)
tariff = result.scalar_one_or_none()
if tariff:
traffic_limit = int(tariff.traffic_limit) if tariff.traffic_limit is not None else None
device_limit = int(tariff.device_limit) if tariff.device_limit is not None else None
else:
logger.warning(f"[LOG] update_subscription: тариф с id={tariff_id} не найден!")
else:
logger.warning(f"[LOG] update_subscription: tariff_id отсутствует!")
await delete_key_from_cluster(old_cluster_id, email, client_id, session=session)
await session.execute(delete(Key).where(Key.tg_id == tg_id, Key.email == email))
await session.commit()
new_cluster_id = (
country_override or cluster_override or await get_least_loaded_cluster(session)
)
new_client_id, remnawave_key = await update_key_on_cluster(
tg_id=tg_id,
client_id=client_id,
email=email,
expiry_time=expiry_time,
cluster_id=new_cluster_id,
session=session,
traffic_limit=traffic_limit,
device_limit=device_limit,
remnawave_link=remnawave_link
)
servers = await get_servers(session)
cluster_servers = servers.get(new_cluster_id)
if cluster_servers is None:
for server_list in servers.values():
for server_info in server_list:
if server_info.get("server_name", "").lower() == new_cluster_id.lower():
cluster_servers = [server_info]
break
if cluster_servers:
break
else:
cluster_servers = []
has_xui = any(s.get("panel_type", "").lower() == "3x-ui" for s in cluster_servers)
final_key_link = public_link if has_xui else None
await store_key(
session=session,
tg_id=tg_id,
client_id=new_client_id,
email=email,
expiry_time=expiry_time,
key=final_key_link,
remnawave_link=remnawave_key,
server_id=new_cluster_id,
tariff_id=tariff_id,
)
async def get_user_traffic(
session: AsyncSession, tg_id: int, email: str
) -> dict[str, Any]:
"""
Получает трафик пользователя на всех серверах, где у него есть ключ (3x-ui и Remnawave).
Для Remnawave трафик считается один раз и отображается как "Remnawave (общий):".
"""
result = await session.execute(
select(Key.client_id, Key.server_id).where(
Key.tg_id == tg_id, Key.email == email
)
)
rows = result.all()
if not rows:
return {"status": "error", "message": "У пользователя нет активных ключей."}
server_ids = {row.server_id for row in rows}
result = await session.execute(
select(Server).where(
Server.server_name.in_(server_ids) | Server.cluster_name.in_(server_ids)
)
)
server_rows = result.scalars().all()
if not server_rows:
logger.error(f"Не найдено серверов для: {server_ids}")
return {
"status": "error",
"message": f"Серверы не найдены: {', '.join(server_ids)}",
}
servers_map = {
s.server_name: {
"server_name": s.server_name,
"cluster_name": s.cluster_name,
"api_url": s.api_url,
"panel_type": s.panel_type,
}
for s in server_rows
}
user_traffic_data = {}
tasks = []
remnawave_client_id = None
remnawave_checked = False
remnawave_api_url = None
async def fetch_traffic(server_info: dict, client_id: str) -> tuple[str, Any]:
server_name = server_info["server_name"]
api_url = server_info["api_url"]
panel_type = server_info.get("panel_type", "3x-ui").lower()
try:
if panel_type == "3x-ui":
xui = await get_xui_instance(api_url)
traffic_info = await get_client_traffic(xui, client_id)
if traffic_info["status"] == "success" and traffic_info["traffic"]:
client_data = traffic_info["traffic"][0]
used_gb = (client_data.up + client_data.down) / 1073741824
return server_name, round(used_gb, 2)
else:
return server_name, "Ошибка получения трафика"
else:
return server_name, f"Неизвестная панель: {panel_type}"
except Exception as e:
return server_name, f"Ошибка: {e}"
for row in rows:
client_id = row.client_id
server_id = row.server_id
matched_servers = [
s
for s in servers_map.values()
if s["server_name"] == server_id or s["cluster_name"] == server_id
]
for server_info in matched_servers:
panel_type = server_info.get("panel_type", "3x-ui").lower()
if panel_type == "remnawave" and not remnawave_checked:
remnawave_client_id = client_id
remnawave_api_url = server_info["api_url"]
remnawave_checked = True
elif panel_type == "3x-ui":
tasks.append(fetch_traffic(server_info, client_id))
results = await asyncio.gather(*tasks, return_exceptions=True)
for server, result in results:
user_traffic_data[server] = result
if remnawave_client_id and remnawave_api_url:
try:
remna = RemnawaveAPI(remnawave_api_url)
if not await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
user_traffic_data["Remnawave (общий)"] = "Не удалось авторизоваться"
else:
user_data = await remna.get_user_by_uuid(remnawave_client_id)
if not user_data:
user_traffic_data["Remnawave (общий)"] = "Клиент не найден"
else:
used_bytes = user_data.get("usedTrafficBytes", 0)
used_gb = round(used_bytes / 1073741824, 2)
user_traffic_data["Remnawave (общий)"] = used_gb
except Exception as e:
user_traffic_data["Remnawave (общий)"] = f"Ошибка: {e}"
return {"status": "success", "traffic": user_traffic_data}
async def toggle_client_on_cluster(
cluster_id: str,
email: str,
client_id: str,
enable: bool = True,
session: AsyncSession = None,
) -> dict[str, Any]:
try:
if session is None:
raise ValueError(
"[Cluster Toggle] Не передан объект сессии для toggle_client_on_cluster"
)
servers = await get_servers(session)
cluster = servers.get(cluster_id)
if not cluster:
found_servers = []
for _, server_list in servers.items():
for server_info in server_list:
if server_info.get("server_name", "").lower() == cluster_id.lower():
found_servers.append(server_info)
if found_servers:
cluster = found_servers
else:
raise ValueError(
f"Кластер или сервер с ID/именем '{cluster_id}' не найден."
)
results = {}
tasks = []
for server_info in cluster:
panel_type = server_info.get("panel_type", "3x-ui").lower()
server_name = server_info.get("server_name", "unknown")
if panel_type == "3x-ui":
inbound_id = server_info.get("inbound_id")
if not inbound_id:
logger.warning(
f"[3x-ui] INBOUND_ID отсутствует для сервера {server_name}. Пропуск."
)
results[server_name] = False
continue
xui = await get_xui_instance(server_info["api_url"])
unique_email = f"{email}_{server_name.lower()}" if SUPERNODE else email
tasks.append(
toggle_client(xui, int(inbound_id), unique_email, client_id, enable)
)
elif panel_type == "remnawave":
remna = RemnawaveAPI(server_info["api_url"])
if not await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
logger.error(
f"[Remnawave] Авторизация не удалась на сервере {server_name}"
)
results[server_name] = False
continue
func = remna.enable_user if enable else remna.disable_user
tasks.append(func(client_id))
else:
logger.warning(
f"[Cluster Toggle] Неизвестный тип панели '{panel_type}' на сервере {server_name}. Пропуск."
)
results[server_name] = False
task_results = await asyncio.gather(*tasks, return_exceptions=True)
for server_info, result in zip(cluster, task_results, strict=False):
server_name = server_info.get("server_name", "unknown")
if isinstance(result, Exception):
logger.error(
f"[Cluster Toggle] Ошибка на сервере {server_name}: {result}"
)
results[server_name] = False
else:
results[server_name] = result
status = "включен" if enable else "отключен"
logger.info(
f"[Cluster Toggle] Клиент {email} {status} на серверах кластера {cluster_id}"
)
logger.info(f"[Cluster Toggle DEBUG] Результаты: {results}")
return {
"status": "success" if any(results.values()) else "error",
"results": results,
}
except Exception as e:
logger.error(
f"[Cluster Toggle] Ошибка при изменении состояния клиента {email} в кластере {cluster_id}: {e}"
)
return {"status": "error", "error": str(e)}
async def reset_traffic_in_cluster(
cluster_id: str, email: str, session: AsyncSession
) -> None:
try:
servers = await get_servers(session)
cluster = servers.get(cluster_id)
if not cluster:
found_servers = []
for _, server_list in servers.items():
for server_info in server_list:
if server_info.get("server_name", "").lower() == cluster_id.lower():
found_servers.append(server_info)
if found_servers:
cluster = found_servers
else:
raise ValueError(
f"Кластер или сервер с ID/именем {cluster_id} не найден."
)
tasks = []
remnawave_done = False
for server_info in cluster:
panel_type = server_info.get("panel_type", "3x-ui").lower()
server_name = server_info.get("server_name", "unknown")
api_url = server_info.get("api_url")
inbound_id = server_info.get("inbound_id")
if panel_type == "remnawave" and not remnawave_done:
result = await session.execute(
select(Key.client_id)
.where(Key.email == email, Key.server_id == cluster_id)
.limit(1)
)
row = result.first()
if not row:
logger.warning(
f"[Remnawave Reset] client_id не найден для {email} на {server_name}"
)
continue
client_id = row[0]
remna = RemnawaveAPI(api_url)
if not await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
logger.warning(
f"[Reset Traffic] Не удалось авторизоваться в Remnawave ({server_name})"
)
continue
tasks.append(remna.reset_user_traffic(client_id))
remnawave_done = True
continue
if panel_type == "3x-ui":
if not inbound_id:
logger.warning(
f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск."
)
continue
xui = await get_xui_instance(api_url)
unique_email = f"{email}_{server_name.lower()}" if SUPERNODE else email
tasks.append(xui.client.reset_stats(int(inbound_id), unique_email))
else:
logger.warning(
f"[Reset Traffic] Неизвестный тип панели '{panel_type}' на {server_name}"
)
await asyncio.gather(*tasks, return_exceptions=True)
logger.info(
f"[Reset Traffic] Трафик клиента {email} успешно сброшен в кластере {cluster_id}"
)
except Exception as e:
logger.error(
f"[Reset Traffic] Ошибка при сбросе трафика клиента {email} в кластере {cluster_id}: {e}"
)
raise