Сгенерил базовую модульную структуру спасибо Claude AI
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m15s
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m15s
This commit is contained in:
parent
a7d24616d7
commit
755280d7de
3
bot/__init__.py
Normal file
3
bot/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
VPN Bot - модульный бот для управления VPN клиентами через XUI панель.
|
||||||
|
"""
|
||||||
12
bot/config.py
Normal file
12
bot/config.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
XUI_FULL_ADDRESS = os.getenv("XUI_FULL_ADDRESS")
|
||||||
|
XUI_PANEL_NAME = os.getenv("XUI_PANEL_NAME")
|
||||||
|
XUI_USERNAME = os.getenv("XUI_USERNAME")
|
||||||
|
XUI_PASSWORD = os.getenv("XUI_PASSWORD")
|
||||||
|
INBOUND_VLESS_ID = int(os.getenv("INBOUND_VLESS_ID", 15))
|
||||||
|
INBOUND_SS_ID = int(os.getenv("INBOUND_SS_ID", 2))
|
||||||
|
SUBSCRIPTION_UUID = os.getenv("SUBSCRIPTION_UUID")
|
||||||
|
SUB_BASE_URL = f"https://udochudo.ru/{SUBSCRIPTION_UUID}/"
|
||||||
|
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||||
|
ALLOWED_CHAT_IDS = set(map(int, os.getenv("ALLOWED_CHAT_IDS", "").split(",")))
|
||||||
3
bot/core/__init__.py
Normal file
3
bot/core/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Ядро приложения - создание бота и диспетчера.
|
||||||
|
"""
|
||||||
23
bot/core/bot.py
Normal file
23
bot/core/bot.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from aiogram import Bot, Dispatcher
|
||||||
|
from aiogram.enums import ParseMode
|
||||||
|
from aiogram.client.default import DefaultBotProperties
|
||||||
|
|
||||||
|
from bot.config import BOT_TOKEN
|
||||||
|
from bot.utils.logging import logger
|
||||||
|
|
||||||
|
|
||||||
|
def create_bot() -> Bot:
|
||||||
|
"""Создание экземпляра бота."""
|
||||||
|
bot = Bot(
|
||||||
|
token=BOT_TOKEN,
|
||||||
|
default=DefaultBotProperties(parse_mode=ParseMode.HTML)
|
||||||
|
)
|
||||||
|
logger.info("Бот создан")
|
||||||
|
return bot
|
||||||
|
|
||||||
|
|
||||||
|
def create_dispatcher() -> Dispatcher:
|
||||||
|
"""Создание диспетчера."""
|
||||||
|
dp = Dispatcher()
|
||||||
|
logger.info("Диспетчер создан")
|
||||||
|
return dp
|
||||||
3
bot/filters/__init__.py
Normal file
3
bot/filters/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Фильтры для обработки сообщений.
|
||||||
|
"""
|
||||||
11
bot/filters/access.py
Normal file
11
bot/filters/access.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from aiogram.types import Message
|
||||||
|
from aiogram.filters import BaseFilter
|
||||||
|
|
||||||
|
from bot.config import ALLOWED_CHAT_IDS
|
||||||
|
|
||||||
|
|
||||||
|
class AllowedUsersFilter(BaseFilter):
|
||||||
|
"""Фильтр для проверки разрешенных пользователей."""
|
||||||
|
|
||||||
|
async def __call__(self, message: Message) -> bool:
|
||||||
|
return message.chat.id in ALLOWED_CHAT_IDS
|
||||||
3
bot/handlers/__init__.py
Normal file
3
bot/handlers/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Обработчики команд и сообщений.
|
||||||
|
"""
|
||||||
29
bot/handlers/base.py
Normal file
29
bot/handlers/base.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from aiogram import types
|
||||||
|
from aiogram.filters import Command
|
||||||
|
|
||||||
|
from bot.utils.logging import logger
|
||||||
|
|
||||||
|
|
||||||
|
async def cmd_start(message: types.Message):
|
||||||
|
"""Обработчик команды /start."""
|
||||||
|
await message.answer(
|
||||||
|
"Привет! Чтобы создать профиль, отправь команду:\n"
|
||||||
|
"<code>/create Telegram id or username</code>\n\n"
|
||||||
|
"Например:\n"
|
||||||
|
"<code>/create udochudo</code>",
|
||||||
|
reply_markup=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def cmd_help(message: types.Message):
|
||||||
|
"""Обработчик команды /help."""
|
||||||
|
help_text = (
|
||||||
|
"Доступные команды:\n\n"
|
||||||
|
"/start или /help - показать это сообщение\n"
|
||||||
|
"/create <telegram_id> - создать профиль клиента\n"
|
||||||
|
"/info <telegram_id> - получить информацию о клиенте\n\n"
|
||||||
|
"Примеры:\n"
|
||||||
|
"<code>/create udochudo</code>\n"
|
||||||
|
"<code>/info udochudo</code>"
|
||||||
|
)
|
||||||
|
await message.answer(help_text)
|
||||||
74
bot/handlers/client.py
Normal file
74
bot/handlers/client.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import json
|
||||||
|
from aiogram import types
|
||||||
|
from aiogram.enums import ParseMode
|
||||||
|
from pyxui.errors import BadLogin
|
||||||
|
|
||||||
|
from bot.services.client import ClientService
|
||||||
|
from bot.utils.logging import logger
|
||||||
|
|
||||||
|
|
||||||
|
class ClientHandlers:
|
||||||
|
"""Обработчики команд для работы с клиентами."""
|
||||||
|
|
||||||
|
def __init__(self, client_service: ClientService):
|
||||||
|
self.client_service = client_service
|
||||||
|
|
||||||
|
async def cmd_create(self, message: types.Message):
|
||||||
|
"""Обработчик команды /create."""
|
||||||
|
args = (message.text or "").strip().split(maxsplit=1)
|
||||||
|
if len(args) < 2:
|
||||||
|
await message.answer(
|
||||||
|
"❌ Укажи Telegram ID или username после команды.\n"
|
||||||
|
"Пример:\n<code>/create udochudo</code>"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
telegram_id = args[1].lstrip("@").strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
success, result_message = await self.client_service.create_client_profile(telegram_id)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
await message.answer(result_message, parse_mode=ParseMode.HTML)
|
||||||
|
else:
|
||||||
|
await message.answer(f"❌ {result_message}")
|
||||||
|
|
||||||
|
except BadLogin:
|
||||||
|
await message.answer("❌ Ошибка: неверный логин или пароль XUI.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Неожиданная ошибка при создании профиля: {e}")
|
||||||
|
await message.answer("❌ Произошла ошибка при создании профиля. Попробуйте позже.")
|
||||||
|
|
||||||
|
async def cmd_info(self, message: types.Message):
|
||||||
|
"""Обработчик команды /info."""
|
||||||
|
args = (message.text or "").strip().split(maxsplit=1)
|
||||||
|
if len(args) < 2:
|
||||||
|
await message.answer(
|
||||||
|
"❌ Укажи Telegram ID или username после команды.\n"
|
||||||
|
"Пример:\n<code>/info udochudo</code>"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
telegram_id = args[1].lstrip("@").strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
vless_client, ss_client = self.client_service.get_client_info(telegram_id)
|
||||||
|
|
||||||
|
def format_info(client, name):
|
||||||
|
if not client:
|
||||||
|
return f"❌ Клиент <b>{name}</b> не найден.\n"
|
||||||
|
json_info = json.dumps(client, ensure_ascii=False, indent=2)
|
||||||
|
return f"🔹 <b>{name}</b>:\n<pre>{json_info}</pre>"
|
||||||
|
|
||||||
|
response = (
|
||||||
|
format_info(vless_client, "VLESS") +
|
||||||
|
"\n\n" +
|
||||||
|
format_info(ss_client, "Shadowsocks")
|
||||||
|
)
|
||||||
|
await message.answer(response, parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
except BadLogin:
|
||||||
|
await message.answer("❌ Ошибка входа в панель XUI.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка получения информации: {e}")
|
||||||
|
await message.answer("❌ Ошибка при получении информации. Проверь лог.")
|
||||||
323
bot/main.py
323
bot/main.py
@ -1,291 +1,62 @@
|
|||||||
import json
|
|
||||||
import logging
|
|
||||||
import uuid
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import aiohttp
|
from aiogram.filters import Command
|
||||||
import os
|
|
||||||
|
from bot.core.bot import create_bot, create_dispatcher
|
||||||
|
from bot.services.xui import XUIService
|
||||||
|
from bot.services.client import ClientService
|
||||||
|
from bot.handlers.base import cmd_start, cmd_help
|
||||||
|
from bot.handlers.client import ClientHandlers
|
||||||
|
from bot.filters.access import AllowedUsersFilter
|
||||||
|
from bot.utils.logging import logger
|
||||||
|
|
||||||
|
|
||||||
from aiogram.types import Message
|
async def setup_handlers(dp, client_handlers: ClientHandlers):
|
||||||
from aiogram import Bot, Dispatcher, types
|
"""Настройка обработчиков команд."""
|
||||||
from aiogram.filters import Command, BaseFilter
|
# Базовые команды (доступны всем)
|
||||||
from aiogram.enums import ParseMode
|
dp.message.register(cmd_start, Command(commands=["start"]))
|
||||||
from aiogram.client.default import DefaultBotProperties
|
dp.message.register(cmd_help, Command(commands=["help"]))
|
||||||
|
|
||||||
from pyxui import XUI # type: ignore
|
# Команды для работы с клиентами (только для разрешенных пользователей)
|
||||||
from pyxui.errors import BadLogin # type: ignore
|
dp.message.register(
|
||||||
|
client_handlers.cmd_create,
|
||||||
|
Command(commands=["create"]),
|
||||||
|
AllowedUsersFilter()
|
||||||
# --- Конфигурация и константы ---
|
)
|
||||||
XUI_FULL_ADDRESS = os.getenv("XUI_FULL_ADDRESS")
|
dp.message.register(
|
||||||
XUI_PANEL_NAME = os.getenv("XUI_PANEL_NAME")
|
client_handlers.cmd_info,
|
||||||
XUI_USERNAME = os.getenv("XUI_USERNAME")
|
Command(commands=["info"]),
|
||||||
XUI_PASSWORD = os.getenv("XUI_PASSWORD")
|
AllowedUsersFilter()
|
||||||
|
)
|
||||||
INBOUND_VLESS_ID = int(os.getenv("INBOUND_VLESS_ID", 15))
|
|
||||||
INBOUND_SS_ID = int(os.getenv("INBOUND_SS_ID", 2))
|
|
||||||
|
|
||||||
SUBSCRIPTION_UUID = os.getenv("SUBSCRIPTION_UUID")
|
|
||||||
SUB_BASE_URL = f"https://udochudo.ru/{SUBSCRIPTION_UUID}/"
|
|
||||||
|
|
||||||
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
|
||||||
|
|
||||||
ALLOWED_CHAT_IDS = set(map(int, os.getenv("ALLOWED_CHAT_IDS", "").split(",")))
|
|
||||||
|
|
||||||
# --- Логирование ---
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# --- Инициализация бота и диспетчера ---
|
|
||||||
bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
|
|
||||||
dp = Dispatcher()
|
|
||||||
|
|
||||||
# --- Экземпляр XUI ---
|
|
||||||
xui = XUI(full_address=XUI_FULL_ADDRESS, panel=XUI_PANEL_NAME, https=True)
|
|
||||||
|
|
||||||
|
|
||||||
def xui_login():
|
|
||||||
if xui.session_string:
|
|
||||||
logger.debug("Уже залогинен в XUI.")
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
xui.login(XUI_USERNAME, XUI_PASSWORD)
|
|
||||||
logger.info("Успешный вход в XUI.")
|
|
||||||
except BadLogin:
|
|
||||||
logger.error("Неверный логин или пароль XUI.")
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при входе в XUI: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
async def create_shadowsocks_client_via_api(telegram_id: str, password: str) -> bool:
|
|
||||||
"""Создание Shadowsocks клиента через API XUI с поддержкой повторного логина."""
|
|
||||||
try:
|
|
||||||
xui_login()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка авторизации в XUI: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
ss_email = f"{telegram_id}_ss"
|
|
||||||
|
|
||||||
settings_str = json.dumps({
|
|
||||||
"clients": [{
|
|
||||||
"method": "chacha20-ietf-poly1305",
|
|
||||||
"password": password,
|
|
||||||
"email": ss_email,
|
|
||||||
"totalGB": 0,
|
|
||||||
"expiryTime": 0,
|
|
||||||
"enable": True,
|
|
||||||
"tgId": telegram_id,
|
|
||||||
"subId": telegram_id,
|
|
||||||
"reset": 0
|
|
||||||
}]
|
|
||||||
}, separators=(',', ':'))
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"id": INBOUND_SS_ID,
|
|
||||||
"settings": settings_str
|
|
||||||
}
|
|
||||||
|
|
||||||
api_url = f"{XUI_FULL_ADDRESS}/xui/API/inbounds/addClient"
|
|
||||||
|
|
||||||
# Получаем куки из сессии XUI
|
|
||||||
cookies = {}
|
|
||||||
|
|
||||||
if hasattr(xui, 'session') and xui.session and hasattr(xui.session, 'cookies'):
|
|
||||||
# Пробуем достать куки в нужном формате
|
|
||||||
for cookie in xui.session.cookies:
|
|
||||||
if cookie.name == 'x-ui':
|
|
||||||
cookies['x-ui'] = cookie.value
|
|
||||||
break
|
|
||||||
|
|
||||||
# Если xui.session_string установлен, используем его как значение cookie x-ui
|
|
||||||
if xui.session_string:
|
|
||||||
cookies['x-ui'] = xui.session_string
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
|
|
||||||
async def do_request():
|
|
||||||
async with session.post(
|
|
||||||
api_url,
|
|
||||||
json=payload,
|
|
||||||
cookies=cookies,
|
|
||||||
headers={
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"User-Agent": "Mozilla/5.0"
|
|
||||||
},
|
|
||||||
ssl=True
|
|
||||||
) as resp:
|
|
||||||
text = await resp.text()
|
|
||||||
logger.debug(f"Response status: {resp.status}")
|
|
||||||
logger.debug(f"Response text: {text}")
|
|
||||||
return resp.status, text
|
|
||||||
|
|
||||||
status, text = await do_request()
|
|
||||||
|
|
||||||
# Если сессия истекла, пробуем повторно залогиниться
|
|
||||||
if status == 401 or (status == 200 and "login" in text.lower()):
|
|
||||||
logger.info("Сессия XUI истекла, повторный вход...")
|
|
||||||
try:
|
|
||||||
xui.session_string = None
|
|
||||||
xui_login()
|
|
||||||
if hasattr(xui, 'session') and xui.session and hasattr(xui.session, 'cookies'):
|
|
||||||
cookies = {cookie.name: cookie.value for cookie in xui.session.cookies}
|
|
||||||
if xui.session_string:
|
|
||||||
cookies['x-ui'] = xui.session_string
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка повторной авторизации в XUI: {e}")
|
|
||||||
return False
|
|
||||||
status, text = await do_request()
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
if "successfully" in text.lower() or "success" in text.lower():
|
|
||||||
logger.info(f"Shadowsocks клиент успешно создан: {ss_email}")
|
|
||||||
return True
|
|
||||||
elif not text.strip():
|
|
||||||
logger.warning(f"Пустой ответ при статусе 200. Проверяем наличие клиента: {ss_email}")
|
|
||||||
return await verify_client_created(telegram_id, ss_email)
|
|
||||||
else:
|
|
||||||
logger.error(f"Неожиданный ответ при создании SS клиента: {text}")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
logger.error(f"Ошибка создания Shadowsocks клиента: {status} {text}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
async def verify_client_created(telegram_id: str, ss_email: str) -> bool:
|
|
||||||
"""Проверка, что клиент существует в inbound SS."""
|
|
||||||
try:
|
|
||||||
inbound_info = xui.get_inbound(INBOUND_SS_ID)
|
|
||||||
if not inbound_info:
|
|
||||||
logger.error(f"Inbound {INBOUND_SS_ID} не найден.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
settings = json.loads(inbound_info.get('settings', '{}'))
|
|
||||||
clients = settings.get('clients', [])
|
|
||||||
|
|
||||||
for client in clients:
|
|
||||||
if client.get('email') == ss_email:
|
|
||||||
logger.info(f"Клиент {ss_email} найден.")
|
|
||||||
return True
|
|
||||||
|
|
||||||
logger.warning(f"Клиент {ss_email} не найден.")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при проверке клиента: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
class AllowedUsersFilter(BaseFilter):
|
|
||||||
async def __call__(self, message: Message) -> bool:
|
|
||||||
return message.chat.id in ALLOWED_CHAT_IDS
|
|
||||||
|
|
||||||
@dp.message(Command(commands=["start","help"]))
|
|
||||||
async def cmd_start(message: types.Message):
|
|
||||||
await message.answer(
|
|
||||||
"Привет! Чтобы создать профиль, отправь команду:\n"
|
|
||||||
"<code>/create Telegram id or username</code>\n\n"
|
|
||||||
"Например:\n"
|
|
||||||
"<code>/create udochudo</code>"
|
|
||||||
,reply_markup=None)
|
|
||||||
|
|
||||||
|
|
||||||
@dp.message(Command(commands=["info"]))
|
|
||||||
async def cmd_info(message: types.Message):
|
|
||||||
args = (message.text or "").strip().split(maxsplit=1)
|
|
||||||
if len(args) < 2:
|
|
||||||
await message.answer(
|
|
||||||
"❌ Укажи Telegram ID или username после команды.\n"
|
|
||||||
"Пример:\n<code>/info udochudo</code>"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
telegram_id = args[1].lstrip("@").strip()
|
|
||||||
vless_email = f"{telegram_id}_vl_ssl"
|
|
||||||
ss_email = f"{telegram_id}_ss"
|
|
||||||
|
|
||||||
try:
|
|
||||||
xui_login()
|
|
||||||
vless_client = xui.get_client(inbound_id=INBOUND_VLESS_ID, email=vless_email)
|
|
||||||
ss_client = xui.get_client(inbound_id=INBOUND_SS_ID, email=ss_email)
|
|
||||||
|
|
||||||
def format_info(client, name):
|
|
||||||
if not client:
|
|
||||||
return f"❌ Клиент <b>{name}</b> не найден.\n"
|
|
||||||
json_info = json.dumps(client, ensure_ascii=False, indent=2)
|
|
||||||
return f"🔹 <b>{name}</b>:\n<pre>{json_info}</pre>"
|
|
||||||
|
|
||||||
response = format_info(vless_client, "VLESS") + "\n\n" + format_info(ss_client, "Shadowsocks")
|
|
||||||
await message.answer(response, parse_mode=ParseMode.HTML)
|
|
||||||
|
|
||||||
except BadLogin:
|
|
||||||
await message.answer("❌ Ошибка входа в панель XUI.")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка получения информации: {e}")
|
|
||||||
await message.answer("❌ Ошибка при получении информации. Проверь лог.")
|
|
||||||
|
|
||||||
|
|
||||||
@dp.message(Command(commands=["create"]))
|
|
||||||
async def cmd_create(message: types.Message):
|
|
||||||
args = (message.text or "").strip().split(maxsplit=1)
|
|
||||||
if len(args) < 2:
|
|
||||||
await message.answer(
|
|
||||||
"❌ Укажи Telegram ID или username после команды.\n"
|
|
||||||
"Пример:\n<code>/create udochudo</code>"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
telegram_id = args[1].lstrip("@").strip()
|
|
||||||
|
|
||||||
try:
|
|
||||||
xui_login()
|
|
||||||
|
|
||||||
vless_email = f"{telegram_id}_vl_ssl"
|
|
||||||
vless_uuid = str(uuid.uuid4())
|
|
||||||
ss_password = str(uuid.uuid4())
|
|
||||||
|
|
||||||
logger.info(f"Создаём клиентов для telegram_id={telegram_id} VLESS email={vless_email}, uuid={vless_uuid}")
|
|
||||||
|
|
||||||
# Создаём VLESS клиента через pyxui
|
|
||||||
xui.add_client(
|
|
||||||
inbound_id=INBOUND_VLESS_ID,
|
|
||||||
email=vless_email,
|
|
||||||
uuid=vless_uuid,
|
|
||||||
enable=True,
|
|
||||||
flow="xtls-rprx-vision",
|
|
||||||
limit_ip=0,
|
|
||||||
total_gb=0,
|
|
||||||
expire_time=0,
|
|
||||||
telegram_id=telegram_id,
|
|
||||||
subscription_id=telegram_id
|
|
||||||
)
|
|
||||||
logger.info(f"VLESS клиент создан с email={vless_email} и uuid={vless_uuid}")
|
|
||||||
|
|
||||||
# Создаём Shadowsocks клиента через API
|
|
||||||
success_ss = await create_shadowsocks_client_via_api(telegram_id, ss_password)
|
|
||||||
if not success_ss:
|
|
||||||
await message.answer("❌ Ошибка при создании Shadowsocks клиента.")
|
|
||||||
return
|
|
||||||
|
|
||||||
subscription_link = f"{SUB_BASE_URL}{telegram_id}?name={telegram_id}"
|
|
||||||
text = f"✅ Профиль для {telegram_id} успешно создан!\n🔗 Подписочная ссылка:\n<code>{subscription_link}</code>"
|
|
||||||
await message.answer(text,parse_mode=ParseMode.HTML)
|
|
||||||
|
|
||||||
except BadLogin:
|
|
||||||
await message.answer("❌ Ошибка: неверный логин или пароль XUI.")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при создании профиля: {e}")
|
|
||||||
await message.answer("❌ Произошла ошибка при создании профиля. Попробуйте позже.")
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
"""Главная функция приложения."""
|
||||||
logger.info("Запуск бота...")
|
logger.info("Запуск бота...")
|
||||||
|
|
||||||
|
# Создаем основные компоненты
|
||||||
|
bot = create_bot()
|
||||||
|
dp = create_dispatcher()
|
||||||
|
|
||||||
|
# Создаем сервисы
|
||||||
|
xui_service = XUIService()
|
||||||
|
client_service = ClientService(xui_service)
|
||||||
|
|
||||||
|
# Создаем обработчики
|
||||||
|
client_handlers = ClientHandlers(client_service)
|
||||||
|
|
||||||
|
# Настраиваем обработчики
|
||||||
|
await setup_handlers(dp, client_handlers)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
logger.info("Бот запущен и готов к работе")
|
||||||
await dp.start_polling(bot)
|
await dp.start_polling(bot)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при запуске бота: {e}")
|
||||||
|
raise
|
||||||
finally:
|
finally:
|
||||||
await bot.session.close()
|
await bot.session.close()
|
||||||
|
logger.info("Бот остановлен")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
3
bot/services/__init__.py
Normal file
3
bot/services/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Сервисы для бизнес-логики приложения.
|
||||||
|
"""
|
||||||
178
bot/services/client.py
Normal file
178
bot/services/client.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from typing import Tuple, Optional, Dict, Any
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from bot.config import (
|
||||||
|
XUI_FULL_ADDRESS,
|
||||||
|
INBOUND_VLESS_ID,
|
||||||
|
INBOUND_SS_ID,
|
||||||
|
SUB_BASE_URL
|
||||||
|
)
|
||||||
|
from bot.services.xui import XUIService
|
||||||
|
from bot.utils.logging import logger
|
||||||
|
|
||||||
|
|
||||||
|
class ClientService:
|
||||||
|
"""Сервис для работы с клиентами VPN."""
|
||||||
|
|
||||||
|
def __init__(self, xui_service: XUIService):
|
||||||
|
self.xui_service = xui_service
|
||||||
|
|
||||||
|
def generate_client_credentials(self, telegram_id: str) -> Tuple[str, str, str, str]:
|
||||||
|
"""Генерация учетных данных для клиента."""
|
||||||
|
vless_email = f"{telegram_id}_vl_ssl"
|
||||||
|
ss_email = f"{telegram_id}_ss"
|
||||||
|
vless_uuid = str(uuid.uuid4())
|
||||||
|
ss_password = str(uuid.uuid4())
|
||||||
|
|
||||||
|
return vless_email, ss_email, vless_uuid, ss_password
|
||||||
|
|
||||||
|
def get_subscription_link(self, telegram_id: str) -> str:
|
||||||
|
"""Получение ссылки на подписку."""
|
||||||
|
return f"{SUB_BASE_URL}{telegram_id}?name={telegram_id}"
|
||||||
|
|
||||||
|
async def create_shadowsocks_client(self, telegram_id: str, password: str) -> bool:
|
||||||
|
"""Создание Shadowsocks клиента через API."""
|
||||||
|
try:
|
||||||
|
self.xui_service.login()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка авторизации в XUI: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
ss_email = f"{telegram_id}_ss"
|
||||||
|
|
||||||
|
settings_str = json.dumps({
|
||||||
|
"clients": [{
|
||||||
|
"method": "chacha20-ietf-poly1305",
|
||||||
|
"password": password,
|
||||||
|
"email": ss_email,
|
||||||
|
"totalGB": 0,
|
||||||
|
"expiryTime": 0,
|
||||||
|
"enable": True,
|
||||||
|
"tgId": telegram_id,
|
||||||
|
"subId": telegram_id,
|
||||||
|
"reset": 0
|
||||||
|
}]
|
||||||
|
}, separators=(',', ':'))
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"id": INBOUND_SS_ID,
|
||||||
|
"settings": settings_str
|
||||||
|
}
|
||||||
|
|
||||||
|
api_url = f"{XUI_FULL_ADDRESS}/xui/API/inbounds/addClient"
|
||||||
|
cookies = self.xui_service.get_cookies()
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async def do_request():
|
||||||
|
async with session.post(
|
||||||
|
api_url,
|
||||||
|
json=payload,
|
||||||
|
cookies=cookies,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": "Mozilla/5.0"
|
||||||
|
},
|
||||||
|
ssl=True
|
||||||
|
) as resp:
|
||||||
|
text = await resp.text()
|
||||||
|
logger.debug(f"Response status: {resp.status}")
|
||||||
|
logger.debug(f"Response text: {text}")
|
||||||
|
return resp.status, text
|
||||||
|
|
||||||
|
status, text = await do_request()
|
||||||
|
|
||||||
|
# Если сессия истекла, пробуем повторно залогиниться
|
||||||
|
if status == 401 or (status == 200 and "login" in text.lower()):
|
||||||
|
logger.info("Сессия XUI истекла, повторный вход...")
|
||||||
|
try:
|
||||||
|
self.xui_service.reset_session()
|
||||||
|
self.xui_service.login()
|
||||||
|
cookies = self.xui_service.get_cookies()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка повторной авторизации в XUI: {e}")
|
||||||
|
return False
|
||||||
|
status, text = await do_request()
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
if "successfully" in text.lower() or "success" in text.lower():
|
||||||
|
logger.info(f"Shadowsocks клиент успешно создан: {ss_email}")
|
||||||
|
return True
|
||||||
|
elif not text.strip():
|
||||||
|
logger.warning(f"Пустой ответ при статусе 200. Проверяем наличие клиента: {ss_email}")
|
||||||
|
return await self.verify_client_created(telegram_id, ss_email)
|
||||||
|
else:
|
||||||
|
logger.error(f"Неожиданный ответ при создании SS клиента: {text}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.error(f"Ошибка создания Shadowsocks клиента: {status} {text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def verify_client_created(self, telegram_id: str, ss_email: str) -> bool:
|
||||||
|
"""Проверка, что клиент существует в inbound SS."""
|
||||||
|
try:
|
||||||
|
inbound_info = self.xui_service.get_inbound(INBOUND_SS_ID)
|
||||||
|
if not inbound_info:
|
||||||
|
logger.error(f"Inbound {INBOUND_SS_ID} не найден.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
settings = json.loads(inbound_info.get('settings', '{}'))
|
||||||
|
clients = settings.get('clients', [])
|
||||||
|
|
||||||
|
for client in clients:
|
||||||
|
if client.get('email') == ss_email:
|
||||||
|
logger.info(f"Клиент {ss_email} найден.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
logger.warning(f"Клиент {ss_email} не найден.")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при проверке клиента: {e}")
|
||||||
|
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]]]:
|
||||||
|
"""Получение информации о клиентах."""
|
||||||
|
vless_email = f"{telegram_id}_vl_ssl"
|
||||||
|
ss_email = f"{telegram_id}_ss"
|
||||||
|
|
||||||
|
vless_client = self.xui_service.get_client(INBOUND_VLESS_ID, vless_email)
|
||||||
|
ss_client = self.xui_service.get_client(INBOUND_SS_ID, ss_email)
|
||||||
|
|
||||||
|
return vless_client, ss_client
|
||||||
110
bot/services/xui.py
Normal file
110
bot/services/xui.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import json
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
|
from pyxui import XUI
|
||||||
|
from pyxui.errors import BadLogin
|
||||||
|
|
||||||
|
from bot.config import (
|
||||||
|
XUI_FULL_ADDRESS,
|
||||||
|
XUI_PANEL_NAME,
|
||||||
|
XUI_USERNAME,
|
||||||
|
XUI_PASSWORD
|
||||||
|
)
|
||||||
|
from bot.utils.logging import logger
|
||||||
|
|
||||||
|
|
||||||
|
class XUIService:
|
||||||
|
"""Сервис для работы с XUI панелью."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.xui = XUI(
|
||||||
|
full_address=XUI_FULL_ADDRESS,
|
||||||
|
panel=XUI_PANEL_NAME,
|
||||||
|
https=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def login(self) -> None:
|
||||||
|
"""Авторизация в XUI панели."""
|
||||||
|
if self.xui.session_string:
|
||||||
|
logger.debug("Уже залогинен в XUI.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.xui.login(XUI_USERNAME, XUI_PASSWORD)
|
||||||
|
logger.info("Успешный вход в XUI.")
|
||||||
|
except BadLogin:
|
||||||
|
logger.error("Неверный логин или пароль XUI.")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при входе в XUI: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_client(self, inbound_id: int, email: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Получение информации о клиенте."""
|
||||||
|
try:
|
||||||
|
self.login()
|
||||||
|
return self.xui.get_client(inbound_id=inbound_id, email=email)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка получения клиента {email}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_vless_client(
|
||||||
|
self,
|
||||||
|
inbound_id: int,
|
||||||
|
email: str,
|
||||||
|
uuid: str,
|
||||||
|
telegram_id: str,
|
||||||
|
enable: bool = True,
|
||||||
|
flow: str = "xtls-rprx-vision",
|
||||||
|
limit_ip: int = 0,
|
||||||
|
total_gb: int = 0,
|
||||||
|
expire_time: int = 0
|
||||||
|
) -> bool:
|
||||||
|
"""Добавление VLESS клиента."""
|
||||||
|
try:
|
||||||
|
self.login()
|
||||||
|
self.xui.add_client(
|
||||||
|
inbound_id=inbound_id,
|
||||||
|
email=email,
|
||||||
|
uuid=uuid,
|
||||||
|
enable=enable,
|
||||||
|
flow=flow,
|
||||||
|
limit_ip=limit_ip,
|
||||||
|
total_gb=total_gb,
|
||||||
|
expire_time=expire_time,
|
||||||
|
telegram_id=telegram_id,
|
||||||
|
subscription_id=telegram_id
|
||||||
|
)
|
||||||
|
logger.info(f"VLESS клиент создан: {email}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка создания VLESS клиента {email}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_inbound(self, inbound_id: int) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Получение информации об inbound."""
|
||||||
|
try:
|
||||||
|
self.login()
|
||||||
|
return self.xui.get_inbound(inbound_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка получения inbound {inbound_id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_cookies(self) -> Dict[str, str]:
|
||||||
|
"""Получение cookies для API запросов."""
|
||||||
|
cookies = {}
|
||||||
|
|
||||||
|
if hasattr(self.xui, 'session') and self.xui.session and hasattr(self.xui.session, 'cookies'):
|
||||||
|
for cookie in self.xui.session.cookies:
|
||||||
|
if cookie.name == 'x-ui':
|
||||||
|
cookies['x-ui'] = cookie.value
|
||||||
|
break
|
||||||
|
|
||||||
|
if self.xui.session_string:
|
||||||
|
cookies['x-ui'] = self.xui.session_string
|
||||||
|
|
||||||
|
return cookies
|
||||||
|
|
||||||
|
def reset_session(self) -> None:
|
||||||
|
"""Сброс сессии для повторной авторизации."""
|
||||||
|
self.xui.session_string = None
|
||||||
3
bot/utils/__init__.py
Normal file
3
bot/utils/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Утилиты и вспомогательные функции.
|
||||||
|
"""
|
||||||
15
bot/utils/logging.py
Normal file
15
bot/utils/logging.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(level: int = logging.INFO) -> logging.Logger:
|
||||||
|
"""Настройка логирования для приложения."""
|
||||||
|
logging.basicConfig(
|
||||||
|
level=level,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
# Создаем общий логгер для всего приложения
|
||||||
|
logger = setup_logging()
|
||||||
Loading…
x
Reference in New Issue
Block a user