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:
parent
ebf1167fea
commit
c2f7d2a88e
@ -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,23 +52,50 @@ 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:
|
||||||
await message.answer("❌ Ошибка входа в панель XUI.")
|
await message.answer("❌ Ошибка входа в панель XUI.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка получения информации: {e}")
|
logger.error(f"Ошибка получения информации: {e}")
|
||||||
await message.answer("❌ Ошибка при получении информации. Проверь лог.")
|
await message.answer("❌ Ошибка при получении информации. Проверь лог.")
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
@ -175,4 +222,25 @@ class ClientService:
|
|||||||
vless_client = self.xui_service.get_client(INBOUND_VLESS_ID, vless_email)
|
vless_client = self.xui_service.get_client(INBOUND_VLESS_ID, vless_email)
|
||||||
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)
|
||||||
@ -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,
|
||||||
|
|||||||
50
bot/utils/XuiServiceWrapper.py
Normal file
50
bot/utils/XuiServiceWrapper.py
Normal 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
|
||||||
Loading…
x
Reference in New Issue
Block a user