import time from dataclasses import dataclass from typing import Any import httpx import py3xui from py3xui import AsyncApi from config import ADMIN_PASSWORD, ADMIN_USERNAME, SUPERNODE, USE_XUI_TOKEN, XUI_TOKEN from logger import logger @dataclass class ClientConfig: """Конфигурация клиента для добавления/обновления.""" client_id: str email: str tg_id: str limit_ip: int total_gb: int expiry_time: int enable: bool flow: str inbound_id: int sub_id: str _xui_instance_cache: dict[str, tuple[AsyncApi, float]] = {} SESSION_TTL = 1800 async def get_xui_instance(api_url: str) -> AsyncApi: key = f"{api_url}|{ADMIN_USERNAME}" current_time = time.time() xui_entry = _xui_instance_cache.get(key) if xui_entry: xui, last_login = xui_entry if current_time - last_login < SESSION_TTL: return xui else: logger.info("[XUI Cache] Сессия устарела (>30 минут), переподключение...") await xui.login() _xui_instance_cache[key] = (xui, current_time) return xui xui = AsyncApi( api_url, ADMIN_USERNAME, ADMIN_PASSWORD, token=XUI_TOKEN if USE_XUI_TOKEN else None, logger=logger, ) await xui.login() _xui_instance_cache[key] = (xui, current_time) return xui async def add_client(xui: py3xui.AsyncApi, config: ClientConfig) -> dict[str, Any]: try: client = py3xui.Client( id=config.client_id, email=config.email.lower(), limit_ip=config.limit_ip if config.limit_ip is not None else 0, total_gb=config.total_gb, expiry_time=config.expiry_time, enable=config.enable, tg_id=config.tg_id, sub_id=config.sub_id, flow=config.flow, ) response = await xui.client.add(config.inbound_id, [client]) logger.info(f"Клиент {config.email} успешно добавлен с ID {config.client_id}") return response if response else {"status": "failed"} except httpx.ConnectTimeout as e: logger.error(f"Ошибка при добавлении клиента {config.email}: {e}") return {"status": "failed", "error": "Timeout"} except Exception as e: error_message = str(e) if "Duplicate email" in error_message: logger.warning(f"Дублированный email: {config.email}. Пропуск. Сообщение: {error_message}") return {"status": "duplicate", "email": config.email} logger.error(f"Ошибка при добавлении клиента {config.email}: {error_message}") return {"status": "failed", "error": error_message} async def extend_client_key( xui: py3xui.AsyncApi, inbound_id: int, email: str, new_expiry_time: int, client_id: str, total_gb: int, sub_id: str, tg_id: int, limit_ip: int = 0, ) -> bool | None: try: client = await xui.client.get_by_email(email) if not client or not client.id: logger.warning(f"Клиент с email {email} не найден или не имеет ID.") return None logger.info(f"Обновление ключа клиента {email} с ID {client.id} до {new_expiry_time}") client.id = client_id client.expiry_time = new_expiry_time client.flow = "xtls-rprx-vision" client.sub_id = sub_id client.total_gb = total_gb client.enable = True client.limit_ip = limit_ip client.inbound_id = inbound_id client.tg_id = tg_id await xui.client.update(client.id, client) await xui.client.reset_stats(inbound_id, email) logger.info(f"Ключ клиента {email} успешно продлён до {new_expiry_time}") return True except httpx.ConnectTimeout as e: logger.error(f"Ошибка при обновлении клиента {email}: {e}") return False except Exception as e: logger.error(f"Ошибка при обновлении клиента с email {email}: {e}") return False async def delete_client( xui: py3xui.AsyncApi, inbound_id: int, email: str, client_id: str, ) -> bool: try: if SUPERNODE: await xui.client.delete(inbound_id, client_id) logger.info(f"Клиент с ID {client_id} был удален успешно (SUPERNODE)") return True client = await xui.client.get_by_email(email) if not client: logger.warning(f"Клиент с email {email} и ID {client_id} не найден") return False client.id = client_id await xui.client.delete(inbound_id, client.id) logger.info(f"Клиент с ID {client_id} был удален успешно") return True except httpx.ConnectTimeout as e: logger.error(f"Ошибка при удалении клиента {email}: {e}") return False except Exception as e: logger.error(f"Ошибка при удалении клиента с ID {client_id}: {e}") return False async def get_client_traffic(xui: py3xui.AsyncApi, client_id: str) -> dict[str, Any]: try: traffic_data = await xui.client.get_traffic_by_id(client_id) if not traffic_data: logger.warning(f"Трафик для клиента {client_id} не найден.") return {"status": "not_found", "client_id": client_id} logger.info(f"Трафик для клиента {client_id} успешно получен.") return {"status": "success", "client_id": client_id, "traffic": traffic_data} except httpx.ConnectTimeout as e: logger.error(f"Ошибка при получении трафика клиента {client_id}: {e}") return {"status": "error", "error": "Timeout"} except Exception as e: logger.error(f"Ошибка при получении трафика клиента {client_id}: {e}") return {"status": "error", "error": str(e)} async def toggle_client( xui: py3xui.AsyncApi, inbound_id: int, email: str, client_id: str, enable: bool = True, ) -> bool: try: client = await xui.client.get_by_email(email) if not client: logger.warning(f"Клиент с email {email} и ID {client_id} не найден.") return False client.sub_id = email client.enable = enable client.id = client_id client.flow = "xtls-rprx-vision" client.limit_ip = 0 client.inbound_id = inbound_id await xui.client.update(client.id, client) status = "включен" if enable else "отключен" logger.info(f"Клиент с email {email} и ID {client_id} успешно {status}.") return True except httpx.ConnectTimeout as e: status = "включении" if enable else "отключении" logger.error(f"Ошибка при {status} клиента с email {email} и ID {client_id}: {e}") return False except Exception as e: status = "включении" if enable else "отключении" logger.error(f"Ошибка при {status} клиента с email {email} и ID {client_id}: {e}") return False def build_vless_link_from_inbound( inbound: py3xui.Inbound, user_uuid: str, email: str, external_host: str, port: int, remark: str | None = None, client_flow: str | None = None, ) -> str: name = remark or email security = (inbound.stream_settings.security or "").lower() network = (inbound.stream_settings.network or "").lower() def _first(val): if isinstance(val, list) and val: return val[0] return val or "" rs = inbound.stream_settings.reality_settings or {} rs_settings = rs.get("settings") or {} pbk = rs_settings.get("publicKey") or rs.get("publicKey") or "" sni = _first( rs.get("serverNames") or rs_settings.get("serverNames") or rs.get("serverName") or rs_settings.get("serverName") ) sid = _first(rs.get("shortIds") or rs_settings.get("shortIds") or rs.get("shortId") or rs_settings.get("shortId")) fp = rs.get("fingerprint") or rs_settings.get("fingerprint") or "" if security == "reality" and network == "tcp": parts = [ f"vless://{user_uuid}@{external_host}:{port}", "?type=tcp&security=reality", f"&pbk={pbk}" if pbk else "", f"&fp={fp}" if fp else "", f"&sni={sni}" if sni else "", f"&sid={sid}" if sid else "", "&spx=%2F", f"&flow={client_flow}" if client_flow else "", f"#{name}", ] return "".join(parts) if network == "ws": ws = inbound.stream_settings.ws_settings or {} path = (ws.get("path") or "/").strip() or "/" host_hdr = external_host if security == "tls": parts = [ f"vless://{user_uuid}@{external_host}:{port}", "?type=ws&security=tls", f"&host={host_hdr}", f"&sni={external_host}", f"&path={path}", f"#{name}", ] return "".join(parts) return f"vless://{user_uuid}@{external_host}:{port}?type=ws&path={path}#{name}" if security == "tls": return f"vless://{user_uuid}@{external_host}:{port}?type=tcp&security=tls&sni={external_host}#{name}" return f"vless://{user_uuid}@{external_host}:{port}?type=tcp#{name}" async def get_vless_link_for_client( xui: py3xui.AsyncApi, inbound_id: int, email: str, external_host: str, port: int, remark: str | None = None, ) -> str | None: try: inbound = await xui.inbound.get_by_id(inbound_id) if not inbound: logger.warning(f"Не удалось собрать VLESS ссылку: inbound_id={inbound_id}, email={email}") return None true_uuid = None client_flow = None if getattr(inbound, "settings", None) and getattr(inbound.settings, "clients", None): for c in inbound.settings.clients: if getattr(c, "email", None) == email: true_uuid = getattr(c, "id", None) client_flow = getattr(c, "flow", None) break if not true_uuid: logger.warning(f"Не удалось получить UUID клиента: inbound_id={inbound_id}, email={email}") return None return build_vless_link_from_inbound( inbound, true_uuid, email, external_host, port, remark, client_flow, ) except Exception as e: logger.error(f"Ошибка при сборке VLESS ссылки: {e}") return None