commit 1f64ae2e35143e704fd1287ebbc90e32dffa8bc9 Author: UdoChudo Date: Mon Jun 9 17:15:37 2025 +0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b853231 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +venv/ +.env +.idea diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e13ecab --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +# Используем официальный python образ +FROM python:3.13-4-slim + +# Устанавливаем рабочую директорию +WORKDIR /app + +# Устанавливаем зависимости +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Копируем исходники +COPY . . + +# Переменные окружения можно установить через docker-compose +ENV PYTHONUNBUFFERED=1 + +# Точка входа — запуск бота +CMD ["python", "bot/main.py"] diff --git a/bot/main.py b/bot/main.py new file mode 100644 index 0000000..4445294 --- /dev/null +++ b/bot/main.py @@ -0,0 +1,291 @@ +import json +import logging +import uuid +import asyncio +import aiohttp +import os + +from aiogram import BaseFilter +from aiogram.types import Message +from aiogram import Bot, Dispatcher, types +from aiogram.filters import Command +from aiogram.enums import ParseMode +from aiogram.client.default import DefaultBotProperties + +from pyxui import XUI # type: ignore +from pyxui.errors import BadLogin # type: ignore + + + +# --- Конфигурация и константы --- +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(","))) + +# --- Логирование --- +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"])) +async def cmd_start(message: types.Message): + await message.answer( + "Привет! Чтобы создать профиль, отправь команду:\n" + "/create telegram_id_or_username\n\n" + "Например:\n" + "/create udochudo" + ) + + +@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/info udochudo" + ) + 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"❌ Клиент {name} не найден.\n" + json_info = json.dumps(client, ensure_ascii=False, indent=2) + return f"🔹 {name}:\n
{json_info}
" + + 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/create udochudo" + ) + 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{subscription_link}" + 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(): + logger.info("Запуск бота...") + try: + await dp.start_polling(bot) + finally: + await bot.session.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..1069dbb --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,13 @@ +version: '3.9' + +services: + telezab-bot: + build: . + container_name: telezab-bot + restart: unless-stopped + env_file: + - stack.env + environment: + - TZ=Asia/Yekaterinburg + volumes: + - /opt/tgbot/logs:/app/logs diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b8d84f9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,22 @@ +aiofiles==24.1.0 +aiogram==3.20.0.post0 +aiohappyeyeballs==2.6.1 +aiohttp==3.11.18 +aiosignal==1.3.2 +annotated-types==0.7.0 +attrs==25.3.0 +certifi==2025.4.26 +charset-normalizer==3.4.2 +frozenlist==1.6.2 +idna==3.10 +magic-filter==1.0.12 +multidict==6.4.4 +propcache==0.3.1 +pydantic==2.11.5 +pydantic_core==2.33.2 +pyxui==1.0.1 +requests==2.32.3 +typing-inspection==0.4.1 +typing_extensions==4.14.0 +urllib3==2.4.0 +yarl==1.20.0