From 310aa9bce60797bc8e4fda8852912917ad0c8085 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Fri, 14 Feb 2025 23:06:45 +0300 Subject: [PATCH] Add client toggle functionality for 3x-ui servers --- backup.py | 2 +- bot.py | 2 +- client.py | 45 ++++++++++- database.py | 2 +- filters/admin.py | 1 - handlers/admin/admin_coupons.py | 2 +- handlers/admin/admin_servers.py | 2 +- handlers/admin/admin_users.py | 2 +- handlers/captcha.py | 2 +- handlers/donate.py | 2 +- handlers/instructions/instructions.py | 2 +- handlers/keys/key_management.py | 8 +- handlers/keys/key_utils.py | 81 ++++++++++++++++++- handlers/keys/keys.py | 4 +- handlers/keys/subscriptions.py | 2 +- .../notifications/general_notifications.py | 2 +- .../notifications/special_notifications.py | 10 +-- handlers/pay.py | 1 - handlers/profile.py | 2 +- handlers/start.py | 4 +- handlers/utils.py | 2 +- keyboards/admin/users_kb.py | 2 +- middlewares/admin.py | 1 - middlewares/session.py | 1 - servers.py | 2 +- 25 files changed, 151 insertions(+), 35 deletions(-) diff --git a/backup.py b/backup.py index 8550e619..eeeec50a 100644 --- a/backup.py +++ b/backup.py @@ -3,8 +3,8 @@ import subprocess from datetime import datetime from aiogram.types import BufferedInputFile - from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT + from logger import logger diff --git a/bot.py b/bot.py index b2f15ad6..a070a71b 100644 --- a/bot.py +++ b/bot.py @@ -8,8 +8,8 @@ from aiogram.filters import ExceptionTypeFilter from aiogram.fsm.storage.memory import MemoryStorage from aiogram.types import BufferedInputFile, ErrorEvent from aiogram.utils.markdown import hbold - from config import ADMIN_ID, API_TOKEN + from filters.private import IsPrivateFilter from logger import logger from middlewares import register_middleware diff --git a/client.py b/client.py index 2d020c2e..ef0643c3 100644 --- a/client.py +++ b/client.py @@ -2,8 +2,8 @@ from dataclasses import dataclass from typing import Any import py3xui - from config import LIMIT_IP, SUPERNODE + from logger import logger @@ -182,3 +182,46 @@ async def get_client_traffic(xui: py3xui.AsyncApi, client_id: str) -> dict[str, 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: + """ + Функция для включения/отключения клиента на сервере 3x-ui. + + Args: + xui: Экземпляр API клиента + inbound_id: ID инбаунда + email: Email клиента + client_id: UUID клиента + enable: True для включения, False для отключения + + Returns: + bool: True при успешном выполнении, False при ошибке + """ + try: + await xui.login() + + # Получаем клиента по email + client = await xui.client.get_by_email(email) + + if not client: + logger.warning(f"Клиент с email {email} и ID {client_id} не найден.") + return False + + # Обновляем параметры клиента + client.enable = enable + client.id = client_id + client.flow = "xtls-rprx-vision" + client.limit_ip = LIMIT_IP + 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 Exception as e: + status = "включении" if enable else "отключении" + logger.error(f"Ошибка при {status} клиента с email {email} и ID {client_id}: {e}") + return False diff --git a/database.py b/database.py index 3b29e7f8..b7a381e1 100644 --- a/database.py +++ b/database.py @@ -4,8 +4,8 @@ from typing import Any import asyncpg import pytz - from config import CASHBACK, CHECK_REFERRAL_REWARD_ISSUED, DATABASE_URL, REFERRAL_BONUS_PERCENTAGES + from logger import logger diff --git a/filters/admin.py b/filters/admin.py index 5a392217..ee324169 100644 --- a/filters/admin.py +++ b/filters/admin.py @@ -1,6 +1,5 @@ from aiogram.filters import BaseFilter from aiogram.types import Message - from config import ADMIN_ID diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index e6edf9f9..d2df9768 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -4,8 +4,8 @@ from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message - from config import USERNAME_BOT + from database import create_coupon, delete_coupon, get_all_coupons from filters.admin import IsAdminFilter from keyboards.admin.coupons_kb import AdminCouponDeleteCallback, build_coupons_kb, build_coupons_list_kb diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 42e65b17..404b9846 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -6,10 +6,10 @@ from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message +from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL from py3xui import AsyncApi from backup import create_backup_and_send_to_admins -from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL from database import check_unique_server_name, delete_server, get_servers from filters.admin import IsAdminFilter from handlers.keys.key_utils import create_key_on_cluster diff --git a/handlers/admin/admin_users.py b/handlers/admin/admin_users.py index 8f22729e..40d381fd 100644 --- a/handlers/admin/admin_users.py +++ b/handlers/admin/admin_users.py @@ -7,8 +7,8 @@ from aiogram.exceptions import TelegramBadRequest from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message - from config import TOTAL_GB + from database import ( delete_key, delete_user_data, diff --git a/handlers/captcha.py b/handlers/captcha.py index 66643304..4f38075c 100644 --- a/handlers/captcha.py +++ b/handlers/captcha.py @@ -6,8 +6,8 @@ from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, Message from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import CAPTCHA_EMOJIS + from logger import logger router = Router() diff --git a/handlers/donate.py b/handlers/donate.py index 73d6f76e..66c1c392 100644 --- a/handlers/donate.py +++ b/handlers/donate.py @@ -3,8 +3,8 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, InlineKeyboardButton, LabeledPrice, Message, PreCheckoutQuery from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import RUB_TO_XTR + from logger import logger diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index db8ec324..f08bf817 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -5,8 +5,8 @@ import aiofiles from aiogram import F, Router, types from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import CONNECT_MACOS, CONNECT_WINDOWS, SUPPORT_CHAT_URL + from database import get_key_details from handlers.texts import ( CONNECT_TV_TEXT, diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 48644a48..8cc01073 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -8,10 +8,6 @@ from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder -from py3xui import AsyncApi - -from bot import bot -from client import delete_client from config import ( ADMIN_PASSWORD, ADMIN_USERNAME, @@ -28,6 +24,10 @@ from config import ( USE_COUNTRY_SELECTION, USE_NEW_PAYMENT_FLOW, ) +from py3xui import AsyncApi + +from bot import bot +from client import delete_client from database import ( add_connection, check_connection_exists, diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index d9f7dbba..f9a6fdfc 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -1,10 +1,10 @@ import asyncio from typing import Any +from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, USE_COUNTRY_SELECTION from py3xui import AsyncApi -from client import ClientConfig, add_client, delete_client, extend_client_key, get_client_traffic -from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, USE_COUNTRY_SELECTION +from client import ClientConfig, add_client, delete_client, extend_client_key, get_client_traffic, toggle_client from database import get_servers, store_key from handlers.utils import get_least_loaded_cluster from logger import logger @@ -364,3 +364,80 @@ async def get_user_traffic(session: Any, tg_id: int, email: str) -> dict[str, An user_traffic_data[server] = result return {"status": "success", "traffic": user_traffic_data} + + +async def toggle_client_on_cluster(cluster_id: str, email: str, client_id: str, enable: bool = True) -> dict[str, Any]: + """ + Включает или отключает клиента на всех серверах указанного кластера. + + Args: + cluster_id (str): ID кластера или имя сервера + email (str): Email клиента + client_id (str): UUID клиента + enable (bool): True для включения, False для отключения + + Returns: + dict[str, Any]: Результат операции с информацией по каждому серверу + """ + try: + servers = await get_servers() + cluster = servers.get(cluster_id) + + if not cluster: + # Поиск по имени сервера, если не найден кластер + found_servers = [] + for _, server_list in servers.items(): + for server_info in server_list: + if server_info.get("server_name", "").lower() == cluster_id.lower(): + found_servers.append(server_info) + + if found_servers: + cluster = found_servers + else: + raise ValueError(f"Кластер или сервер с ID/именем {cluster_id} не найден.") + + results = {} + tasks = [] + + for server_info in cluster: + xui = AsyncApi( + server_info["api_url"], + username=ADMIN_USERNAME, + password=ADMIN_PASSWORD, + ) + + inbound_id = server_info.get("inbound_id") + server_name = server_info.get("server_name", "unknown") + + if not inbound_id: + logger.warning(f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск.") + results[server_name] = False + continue + + if SUPERNODE: + unique_email = f"{email}_{server_name.lower()}" + else: + unique_email = email + + tasks.append(toggle_client(xui, int(inbound_id), unique_email, client_id, enable)) + + # Выполняем все задачи параллельно + task_results = await asyncio.gather(*tasks, return_exceptions=True) + + # Формируем результаты для каждого сервера + for server_info, result in zip(cluster, task_results, strict=False): + server_name = server_info.get("server_name", "unknown") + if isinstance(result, Exception): + logger.error(f"Ошибка на сервере {server_name}: {result}") + results[server_name] = False + else: + results[server_name] = result + + status = "включен" if enable else "отключен" + logger.info(f"Клиент {email} {status} на серверах кластера {cluster_id}") + + return {"status": "success" if any(results.values()) else "error", "results": results} + + except Exception as e: + logger.error(f"Ошибка при изменении состояния клиента {email} в кластере {cluster_id}: {e}") + return {"status": "error", "error": str(e)} diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index a8045e5c..88695fda 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -10,8 +10,6 @@ import pytz from aiogram import F, Router, types from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder - -from bot import bot from config import ( CONNECT_ANDROID, CONNECT_IOS, @@ -27,6 +25,8 @@ from config import ( USE_COUNTRY_SELECTION, USE_NEW_PAYMENT_FLOW, ) + +from bot import bot from database import ( check_server_name_by_cluster, create_temporary_data, diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index 2d66e99b..940131e4 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -5,8 +5,8 @@ from datetime import datetime import aiohttp import asyncpg from aiohttp import web - from config import DATABASE_URL, PROJECT_NAME, SUB_MESSAGE, SUPERNODE, TRANSITION_DATE_STR, USE_COUNTRY_SELECTION + from database import get_key_details, get_servers from logger import logger diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py index c87fe242..fa05d8ef 100644 --- a/handlers/notifications/general_notifications.py +++ b/handlers/notifications/general_notifications.py @@ -4,7 +4,6 @@ from datetime import datetime, timedelta import asyncpg import pytz from aiogram import Bot, Router - from config import ( DATABASE_URL, NOTIFICATION_TIME, @@ -17,6 +16,7 @@ from config import ( TOTAL_GB, TRIAL_TIME_DISABLE, ) + from database import ( add_notification, check_notification_time, diff --git a/handlers/notifications/special_notifications.py b/handlers/notifications/special_notifications.py index ac458b21..27e8ceb2 100644 --- a/handlers/notifications/special_notifications.py +++ b/handlers/notifications/special_notifications.py @@ -1,11 +1,13 @@ import asyncio +from datetime import datetime, timedelta import asyncpg +import pytz from aiogram import Bot, Router, types from aiogram.exceptions import TelegramForbiddenError from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import NOTIFY_EXTRA_DAYS, NOTIFY_INACTIVE, NOTIFY_INACTIVE_TRAFFIC, SUPPORT_CHAT_URL, TRIAL_TIME + from database import ( add_notification, check_notification_time, @@ -13,14 +15,12 @@ from database import ( ) from handlers.keys.key_utils import get_user_traffic from logger import logger -from datetime import datetime, timedelta - -import pytz router = Router() moscow_tz = pytz.timezone("Europe/Moscow") + async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): """ Проверяет пользователей, не активировавших пробный период, и отправляет им напоминания. @@ -200,4 +200,4 @@ async def notify_users_no_traffic(bot: Bot, conn: asyncpg.Connection, current_ti except Exception as e: logger.error(f"Ошибка обновления notified для пользователя {tg_id}: {e}") - logger.info("✅ Обработка пользователей с нулевым трафиком завершена.") \ No newline at end of file + logger.info("✅ Обработка пользователей с нулевым трафиком завершена.") diff --git a/handlers/pay.py b/handlers/pay.py index 44b487a9..768cddd8 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -1,7 +1,6 @@ from aiogram import F, Router from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import ( CRYPTO_BOT_ENABLE, DONATIONS_ENABLE, diff --git a/handlers/profile.py b/handlers/profile.py index 232fa180..db25f06e 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -7,8 +7,8 @@ from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import DATABASE_URL, INSTRUCTIONS_BUTTON, NEWS_MESSAGE, RENEWAL_PLANS + from database import get_balance, get_key_count, get_last_payments, get_referral_stats, get_trial from handlers.buttons.profile import ( ADD_SUB, diff --git a/handlers/start.py b/handlers/start.py index d8f590de..965550f6 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -12,8 +12,6 @@ from aiogram.types import ( Message, ) from aiogram.utils.keyboard import InlineKeyboardBuilder - -from bot import bot from config import ( CAPTCHA_ENABLE, CHANNEL_EXISTS, @@ -23,6 +21,8 @@ from config import ( DONATIONS_ENABLE, SUPPORT_CHAT_URL, ) + +from bot import bot from database import ( add_connection, add_referral, diff --git a/handlers/utils.py b/handlers/utils.py index 3a4acc2b..33207392 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -5,9 +5,9 @@ import string import aiohttp import asyncpg +from config import DATABASE_URL from bot import bot -from config import DATABASE_URL from database import get_all_keys, get_servers from logger import logger diff --git a/keyboards/admin/users_kb.py b/keyboards/admin/users_kb.py index be179bc1..52931cf9 100644 --- a/keyboards/admin/users_kb.py +++ b/keyboards/admin/users_kb.py @@ -3,8 +3,8 @@ from datetime import datetime, timezone from aiogram.filters.callback_data import CallbackData from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import RENEWAL_PRICES + from keyboards.admin.panel_kb import build_admin_back_btn diff --git a/middlewares/admin.py b/middlewares/admin.py index bc21afe4..85819d32 100644 --- a/middlewares/admin.py +++ b/middlewares/admin.py @@ -3,7 +3,6 @@ from typing import Any from aiogram import BaseMiddleware from aiogram.types import TelegramObject - from config import ADMIN_ID diff --git a/middlewares/session.py b/middlewares/session.py index a5d34441..302523f4 100644 --- a/middlewares/session.py +++ b/middlewares/session.py @@ -4,7 +4,6 @@ from typing import Any import asyncpg from aiogram import BaseMiddleware from aiogram.types import TelegramObject - from config import DATABASE_URL diff --git a/servers.py b/servers.py index 536e3823..4458374d 100644 --- a/servers.py +++ b/servers.py @@ -5,10 +5,10 @@ from datetime import datetime, timedelta import asyncpg from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder +from config import ADMIN_ID, DATABASE_URL, PING_TIME from ping3 import ping from bot import bot -from config import ADMIN_ID, DATABASE_URL, PING_TIME from database import check_unique_server_name, create_server, get_servers from logger import logger