diff --git a/bot.py b/bot.py
index ce621a3c..f1535263 100644
--- a/bot.py
+++ b/bot.py
@@ -19,7 +19,7 @@ bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTM
storage = MemoryStorage()
dp = Dispatcher(bot=bot, storage=storage)
-version = "4.2-b220422"
+version = "4.2-b220424"
register_middleware(dp)
diff --git a/handlers/admin/ads/ads_handler.py b/handlers/admin/ads/ads_handler.py
index eb8c1647..601b58bd 100644
--- a/handlers/admin/ads/ads_handler.py
+++ b/handlers/admin/ads/ads_handler.py
@@ -20,6 +20,7 @@ from .keyboard import (
build_ads_list_kb,
build_ads_stats_kb,
build_ads_delete_confirm_kb,
+ build_cancel_input_kb
)
router = Router()
@@ -40,7 +41,11 @@ async def handle_ads_menu(callback_query: CallbackQuery):
@router.callback_query(AdminAdsCallback.filter(F.action == "create"), IsAdminFilter())
async def handle_ads_create(callback_query: CallbackQuery, state: FSMContext):
await state.set_state(AdminAdsState.waiting_for_new_name)
- await callback_query.message.edit_text("📝 Введите название новой ссылки:")
+ await callback_query.message.edit_text(
+ "📝 Введите название новой ссылки:",
+ reply_markup=build_cancel_input_kb()
+ )
+
@router.message(AdminAdsState.waiting_for_new_name, IsAdminFilter())
@@ -48,7 +53,11 @@ async def handle_ads_name_input(message: Message, state: FSMContext):
name = message.text.strip()
await state.update_data(name=name)
await state.set_state(AdminAdsState.waiting_for_new_code)
- await message.answer(f"🔗 Введите код ссылки для: {name}.")
+ await message.answer(
+ f"🔗 Введите код ссылки для: {name}.",
+ reply_markup=build_cancel_input_kb()
+ )
+
@router.message(AdminAdsState.waiting_for_new_code, IsAdminFilter())
@@ -153,3 +162,12 @@ def format_ads_stats(stats: dict, username_bot: str) -> str:
f"Просмотр статистики и управление рекламными ссылками."
)
+
+
+@router.callback_query(AdminAdsCallback.filter(F.action == "cancel_input"), IsAdminFilter())
+async def handle_ads_cancel_input(callback_query: CallbackQuery, state: FSMContext):
+ await state.clear()
+ await callback_query.message.edit_text(
+ text="📊 Аналитика рекламы:",
+ reply_markup=build_ads_kb()
+ )
diff --git a/handlers/admin/ads/keyboard.py b/handlers/admin/ads/keyboard.py
index 24a07afd..fb46a531 100644
--- a/handlers/admin/ads/keyboard.py
+++ b/handlers/admin/ads/keyboard.py
@@ -14,6 +14,7 @@ def build_ads_kb() -> InlineKeyboardMarkup:
builder.button(text="➕ Новая ссылка", callback_data=AdminAdsCallback(action="create").pack())
builder.button(text="📊 Список", callback_data=AdminAdsCallback(action="list").pack())
builder.row(build_admin_back_btn())
+ builder.adjust(1)
return builder.as_markup()
@@ -66,4 +67,13 @@ def build_ads_delete_confirm_kb(code: str) -> InlineKeyboardMarkup:
callback_data=AdminAdsCallback(action="view", code=code).pack()
)
builder.adjust(1)
- return builder.as_markup()
\ No newline at end of file
+ return builder.as_markup()
+
+
+def build_cancel_input_kb() -> InlineKeyboardMarkup:
+ builder = InlineKeyboardBuilder()
+ builder.button(
+ text="❌ Отмена",
+ callback_data=AdminAdsCallback(action="cancel_input", code="none").pack()
+ )
+ return builder.as_markup()
diff --git a/handlers/keys/key_mode/key_country_mode.py b/handlers/keys/key_mode/key_country_mode.py
index f8bda9e3..95f1ac6e 100644
--- a/handlers/keys/key_mode/key_country_mode.py
+++ b/handlers/keys/key_mode/key_country_mode.py
@@ -450,12 +450,26 @@ async def finalize_key_creation(
await state.clear()
-async def check_server_availability(server_info: dict) -> bool:
+async def check_server_availability(server_info: dict, session: Any) -> bool:
"""
- Проверяет доступность сервера (3x-ui или Remnawave).
+ Проверяет доступность сервера (3x-ui или Remnawave),
+ а также включён ли он и не превышен ли лимит ключей (max_keys).
"""
- panel_type = server_info.get("panel_type", "3x-ui").lower()
server_name = server_info.get("server_name", "unknown")
+ panel_type = server_info.get("panel_type", "3x-ui").lower()
+ enabled = server_info.get("enabled", True)
+ max_keys = server_info.get("max_keys")
+
+ if not enabled:
+ logger.info(f"[Ping] Сервер {server_name} выключен (enabled = FALSE).")
+ return False
+
+ if max_keys is not None:
+ count_query = "SELECT COUNT(*) FROM keys WHERE server_id = $1"
+ key_count = await session.fetchval(count_query, server_name)
+ if key_count >= max_keys:
+ logger.info(f"[Ping] Сервер {server_name} достиг лимита ключей: {key_count}/{max_keys}.")
+ return False
try:
if panel_type == "remnawave":
@@ -480,4 +494,4 @@ async def check_server_availability(server_info: dict) -> bool:
return False
except Exception as e:
logger.warning(f"[Ping] Ошибка при проверке сервера {server_name}: {e}")
- return False
+ return False
\ No newline at end of file
diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py
index e3743219..86c5c458 100644
--- a/handlers/keys/key_utils.py
+++ b/handlers/keys/key_utils.py
@@ -551,14 +551,7 @@ async def update_subscription(tg_id: int, email: str, session: Any, cluster_over
async def get_user_traffic(session: Any, tg_id: int, email: str) -> dict[str, Any]:
"""
Получает трафик пользователя на всех серверах, где у него есть ключ (3x-ui и Remnawave).
-
- Args:
- session (Any): Сессия базы данных.
- tg_id (int): ID пользователя Telegram.
- email (str): Email пользователя.
-
- Returns:
- dict[str, Any]: Структура с данными о трафике.
+ Для Remnawave трафик считается один раз и отображается как "Remnawave (общий):".
"""
query = "SELECT client_id, server_id FROM keys WHERE tg_id = $1 AND email = $2"
rows = await session.fetch(query, tg_id, email)
@@ -582,6 +575,10 @@ async def get_user_traffic(session: Any, tg_id: int, email: str) -> dict[str, An
servers_map = {row["server_name"]: row for row in server_rows}
user_traffic_data = {}
+ tasks = []
+
+ remnawave_client_id = None
+ remnawave_checked = False
async def fetch_traffic(server_info: dict, client_id: str) -> tuple[str, Any]:
server_name = server_info["server_name"]
@@ -598,28 +595,11 @@ async def get_user_traffic(session: Any, tg_id: int, email: str) -> dict[str, An
return server_name, round(used_gb, 2)
else:
return server_name, "Ошибка получения трафика"
-
- elif panel_type == "remnawave":
- remna = RemnawaveAPI(api_url)
- logged_in = await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD)
- if not logged_in:
- return server_name, "Не удалось авторизоваться"
-
- user_data = await remna.get_user_by_uuid(client_id)
- if not user_data:
- return server_name, "Клиент не найден"
-
- used_bytes = user_data.get("usedTrafficBytes", 0)
- used_gb = used_bytes / 1073741824
- return server_name, round(used_gb, 2)
-
else:
return server_name, f"Неизвестная панель: {panel_type}"
-
except Exception as e:
return server_name, f"Ошибка: {e}"
- tasks = []
for row in rows:
client_id = row["client_id"]
server_id = row["server_id"]
@@ -628,12 +608,36 @@ async def get_user_traffic(session: Any, tg_id: int, email: str) -> dict[str, An
s for s in servers_map.values() if s["server_name"] == server_id or s["cluster_name"] == server_id
]
for server_info in matched_servers:
- tasks.append(fetch_traffic(server_info, client_id))
+ panel_type = server_info.get("panel_type", "3x-ui").lower()
+
+ if panel_type == "remnawave" and not remnawave_checked:
+ remnawave_client_id = client_id
+ remnawave_api_url = server_info["api_url"]
+ remnawave_checked = True
+ elif panel_type == "3x-ui":
+ tasks.append(fetch_traffic(server_info, client_id))
results = await asyncio.gather(*tasks, return_exceptions=True)
+
for server, result in results:
user_traffic_data[server] = result
+ if remnawave_client_id:
+ try:
+ remna = RemnawaveAPI(remnawave_api_url)
+ if not await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
+ user_traffic_data["Remnawave (общий)"] = "Не удалось авторизоваться"
+ else:
+ user_data = await remna.get_user_by_uuid(remnawave_client_id)
+ if not user_data:
+ user_traffic_data["Remnawave (общий)"] = "Клиент не найден"
+ else:
+ used_bytes = user_data.get("usedTrafficBytes", 0)
+ used_gb = round(used_bytes / 1073741824, 2)
+ user_traffic_data["Remnawave (общий)"] = used_gb
+ except Exception as e:
+ user_traffic_data["Remnawave (общий)"] = f"Ошибка: {e}"
+
return {"status": "success", "traffic": user_traffic_data}