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: """ Удаляет клиента с сервера 3x-ui. """ 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