Add client toggle functionality for 3x-ui servers

This commit is contained in:
Zakhar Izmaylov
2025-02-14 23:06:45 +03:00
parent 2dcd1e65db
commit 310aa9bce6
25 changed files with 151 additions and 35 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+44 -1
View File
@@ -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
+1 -1
View File
@@ -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
-1
View File
@@ -1,6 +1,5 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message
from config import ADMIN_ID
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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,
+4 -4
View File
@@ -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,
+79 -2
View File
@@ -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)}
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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
@@ -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,
@@ -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("✅ Обработка пользователей с нулевым трафиком завершена.")
logger.info("✅ Обработка пользователей с нулевым трафиком завершена.")
-1
View File
@@ -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,
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
-1
View File
@@ -3,7 +3,6 @@ from typing import Any
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from config import ADMIN_ID
-1
View File
@@ -4,7 +4,6 @@ from typing import Any
import asyncpg
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from config import DATABASE_URL
+1 -1
View File
@@ -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