feat: /info command now show full stats and config in vless and ss inbounds

feat: /create command now check profile existed in vless and ss inbounds and if yes just send sub link
This commit is contained in:
Udo Chudo 2025-08-31 23:56:35 +05:00
parent ebf1167fea
commit c2f7d2a88e
4 changed files with 204 additions and 15 deletions

View File

@ -40,7 +40,7 @@ class ClientHandlers:
await message.answer(f"❌ Произошла ошибка при создании профиля. Попробуйте позже.\n {e}") await message.answer(f"❌ Произошла ошибка при создании профиля. Попробуйте позже.\n {e}")
async def cmd_info(self, message: types.Message): async def cmd_info(self, message: types.Message):
"""Обработчик команды /info.""" """Обработчик команды /info с выводом конфига и статистики."""
args = (message.text or "").strip().split(maxsplit=1) args = (message.text or "").strip().split(maxsplit=1)
if len(args) < 2: if len(args) < 2:
await message.answer( await message.answer(
@ -52,19 +52,45 @@ class ClientHandlers:
telegram_id = args[1].lstrip("@").strip() telegram_id = args[1].lstrip("@").strip()
try: try:
vless_client, ss_client = self.client_service.get_client_info(telegram_id) (vless_client, vless_stats), (ss_client, ss_stats) = \
self.client_service.get_client_info_with_stats(telegram_id)
def format_info(client, name): def format_info(client, stats, name):
if not client: if not client:
return f"❌ Клиент <b>{name}</b> не найден.\n" return f"❌ Клиент <b>{name}</b> не найден.\n"
# Конфиг клиента
json_info = json.dumps(client, ensure_ascii=False, indent=2) json_info = json.dumps(client, ensure_ascii=False, indent=2)
return f"🔹 <b>{name}</b>:\n<pre>{json_info}</pre>" text = f"🔹 <b>{name}</b>:\n<pre>{json_info}</pre>"
# Статистика
if stats:
up = stats.get("up", 0)
down = stats.get("down", 0)
total = up + down
def human_size(num):
for unit in ["B", "KB", "MB", "GB", "TB"]:
if num < 1024:
return f"{num:.2f} {unit}"
num /= 1024
return f"{num:.2f} PB"
text += (
f"\n📊 Трафик: ↑ {human_size(up)} / ↓ {human_size(down)}"
f"{human_size(total)})"
)
else:
text += "\n📊 Статистика не найдена."
return text
response = ( response = (
format_info(vless_client, "VLESS") + format_info(vless_client, vless_stats, "VLESS")
"\n\n" + + "\n\n"
format_info(ss_client, "Shadowsocks") + format_info(ss_client, ss_stats, "Shadowsocks")
) )
await message.answer(response, parse_mode=ParseMode.HTML) await message.answer(response, parse_mode=ParseMode.HTML)
except BadLogin: except BadLogin:
@ -72,3 +98,4 @@ class ClientHandlers:
except Exception as e: except Exception as e:
logger.error(f"Ошибка получения информации: {e}") logger.error(f"Ошибка получения информации: {e}")
await message.answer("❌ Ошибка при получении информации. Проверь лог.") await message.answer("❌ Ошибка при получении информации. Проверь лог.")

View File

@ -132,10 +132,20 @@ class ClientService:
return False return False
async def create_client_profile(self, telegram_id: str) -> Tuple[bool, str]: async def create_client_profile(self, telegram_id: str) -> Tuple[bool, str]:
"""Создание полного профиля клиента (VLESS + Shadowsocks).""" """Создание полного профиля клиента (VLESS + Shadowsocks) с проверкой существующего."""
try: try:
vless_email, ss_email, vless_uuid, ss_password = self.generate_client_credentials(telegram_id) # Сначала проверяем, есть ли уже клиенты
vless_client, ss_client = self.get_client_info(telegram_id)
if vless_client or ss_client:
subscription_link = self.get_subscription_link(telegram_id)
logger.info(f"Клиент для {telegram_id} уже существует.")
return True, (
f" Клиент для <b>{telegram_id}</b> уже существует.\n"
f"🔗 Подписочная ссылка:\n<code>{subscription_link}</code>"
)
# Генерируем новые креды
vless_email, ss_email, vless_uuid, ss_password = self.generate_client_credentials(telegram_id)
logger.info(f"Создаём клиентов для telegram_id={telegram_id}") logger.info(f"Создаём клиентов для telegram_id={telegram_id}")
# Создаём VLESS клиента # Создаём VLESS клиента
@ -151,13 +161,13 @@ class ClientService:
# Создаём Shadowsocks клиента # Создаём Shadowsocks клиента
ss_success = await self.create_shadowsocks_client(telegram_id, ss_password) ss_success = await self.create_shadowsocks_client(telegram_id, ss_password)
if not ss_success: if not ss_success:
return False, "Ошибка при создании Shadowsocks клиента" return False, "Ошибка при создании Shadowsocks клиента"
# Подписочная ссылка
subscription_link = self.get_subscription_link(telegram_id) subscription_link = self.get_subscription_link(telegram_id)
success_message = ( success_message = (
f"✅ Профиль для {telegram_id} успешно создан!\n" f"✅ Профиль для <b>{telegram_id}</b> успешно создан!\n"
f"🔗 Подписочная ссылка:\n<code>{subscription_link}</code>" f"🔗 Подписочная ссылка:\n<code>{subscription_link}</code>"
) )
@ -167,6 +177,43 @@ class ClientService:
logger.error(f"Ошибка при создании профиля: {e}") logger.error(f"Ошибка при создании профиля: {e}")
return False, "Произошла ошибка при создании профиля" return False, "Произошла ошибка при создании профиля"
# async def create_client_profile(self, telegram_id: str) -> Tuple[bool, str]:
# """Создание полного профиля клиента (VLESS + Shadowsocks)."""
# try:
# vless_email, ss_email, vless_uuid, ss_password = self.generate_client_credentials(telegram_id)
#
# logger.info(f"Создаём клиентов для telegram_id={telegram_id}")
#
# # Создаём VLESS клиента
# vless_success = self.xui_service.add_vless_client(
# inbound_id=INBOUND_VLESS_ID,
# email=vless_email,
# uuid=vless_uuid,
# telegram_id=telegram_id
# )
#
# if not vless_success:
# return False, "Ошибка при создании VLESS клиента"
#
# # Создаём Shadowsocks клиента
# ss_success = await self.create_shadowsocks_client(telegram_id, ss_password)
#
# if not ss_success:
# return False, "Ошибка при создании Shadowsocks клиента"
#
# subscription_link = self.get_subscription_link(telegram_id)
# success_message = (
# f"✅ Профиль для {telegram_id} успешно создан!\n"
# f"🔗 Подписочная ссылка:\n<code>{subscription_link}</code>"
# )
#
# return True, success_message
#
# except Exception as e:
# logger.error(f"Ошибка при создании профиля: {e}")
# return False, "Произошла ошибка при создании профиля"
def get_client_info(self, telegram_id: str) -> Tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]: def get_client_info(self, telegram_id: str) -> Tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]:
"""Получение информации о клиентах.""" """Получение информации о клиентах."""
vless_email = f"{telegram_id}_vl_ssl" vless_email = f"{telegram_id}_vl_ssl"
@ -176,3 +223,24 @@ class ClientService:
ss_client = self.xui_service.get_client(INBOUND_SS_ID, ss_email) ss_client = self.xui_service.get_client(INBOUND_SS_ID, ss_email)
return vless_client, ss_client return vless_client, ss_client
def get_client_info_with_stats(self, telegram_id: str):
"""Возвращает клиентов и их статистику по Telegram ID."""
vless_client, ss_client = self.get_client_info(telegram_id)
vless_stats = None
ss_stats = None
if vless_client:
vless_stats = self.xui_service.get_client_stats(
inbound_id=INBOUND_VLESS_ID,
email=vless_client["email"]
)
if ss_client:
ss_stats = self.xui_service.get_client_stats(
inbound_id=INBOUND_SS_ID,
email=ss_client["email"]
)
return (vless_client, vless_stats), (ss_client, ss_stats)

View File

@ -40,14 +40,58 @@ class XUIService:
raise raise
def get_client(self, inbound_id: int, email: str) -> Optional[Dict[str, Any]]: def get_client(self, inbound_id: int, email: str) -> Optional[Dict[str, Any]]:
"""Получение информации о клиенте.""" """Получение информации о клиенте (работает и для SS, и для VLESS)."""
try: try:
self.login() self.login()
return self.xui.get_client(inbound_id=inbound_id, email=email) inbounds = self.xui.get_inbounds()
if "obj" not in inbounds:
logger.error("Некорректный ответ от XUI при запросе inbounds")
return None
for inbound in inbounds["obj"]:
if inbound["id"] != inbound_id:
continue
settings = json.loads(inbound["settings"])
for client in settings.get("clients", []):
if client.get("email") == email:
# У SS-клиентов id нет — подставляем email
if "id" not in client:
client["id"] = client["email"]
return client
logger.warning(f"Клиент {email} не найден в inbound {inbound_id}")
return None
except Exception as e: except Exception as e:
logger.error(f"Ошибка получения клиента {email}: {e}") logger.error(f"Ошибка получения клиента {email}: {e}")
return None return None
def get_client_stats(self, inbound_id: int, email: str) -> Optional[Dict[str, Any]]:
"""Получение статистики клиента по inbound и email."""
try:
self.login()
inbounds = self.xui.get_inbounds()
if "obj" not in inbounds:
logger.error("Некорректный ответ от XUI при запросе inbounds")
return None
for inbound in inbounds["obj"]:
if inbound["id"] != inbound_id:
continue
for client_stat in inbound.get("clientStats", []):
if client_stat.get("email") == email:
return client_stat
logger.warning(f"Статистика по клиенту {email} не найдена в inbound {inbound_id}")
return None
except Exception as e:
logger.error(f"Ошибка получения статистики клиента {email}: {e}")
return None
def add_vless_client( def add_vless_client(
self, self,
inbound_id: int, inbound_id: int,

View File

@ -0,0 +1,50 @@
import json
import logging
from typing import Optional, Union
from pyxui import XUI, errors
logger = logging.getLogger(__name__)
class XuiServiceWrapper:
def __init__(self, host: str, username: str, password: str):
self.xui = XUI(host, username, password)
def login(self):
"""Авторизация в XUI"""
return self.xui.login()
def get_client(self, inbound_id: int, email: Optional[str] = None, uuid: Optional[str] = None) -> Union[dict, None]:
"""Безопасное получение клиента (работает и для SS, и для VLESS/VMess)"""
try:
inbounds = self.xui.get_inbounds()
for inbound in inbounds["obj"]:
if inbound["id"] != inbound_id:
continue
settings = json.loads(inbound["settings"])
for client in settings["clients"]:
if (email and client.get("email") == email) or (uuid and client.get("id") == uuid):
return client
return None
except errors.NotFound:
logger.warning(f"Клиент не найден: inbound_id={inbound_id}, email={email}, uuid={uuid}")
return None
except Exception as e:
logger.error(f"Ошибка при поиске клиента {email or uuid}: {e}")
return None
def get_client_stats(self, inbound_id: int, email: str) -> Union[dict, None]:
"""Получение статистики клиента"""
try:
inbounds = self.xui.get_inbounds()
for inbound in inbounds["obj"]:
if inbound["id"] != inbound_id:
continue
for client in inbound.get("clientStats", []):
if client.get("email") == email:
return client
return None
except Exception as e:
logger.error(f"Ошибка при получении статистики клиента {email}: {e}")
return None