add traffic statistic/share refferal link and more

This commit is contained in:
Vladless
2025-02-07 21:38:46 +03:00
parent 0789b3fe9d
commit 5a77f29bd5
9 changed files with 1410 additions and 1167 deletions
+28 -1
View File
@@ -2,8 +2,8 @@ from dataclasses import dataclass
from typing import Any
import py3xui
from config import LIMIT_IP, SUPERNODE
from config import LIMIT_IP, SUPERNODE
from logger import logger
@@ -155,3 +155,30 @@ async def delete_client(
except Exception as e:
logger.error(f"Ошибка при удалении клиента с ID {client_id}: {e}")
return False
async def get_client_traffic(xui: py3xui.AsyncApi, client_id: str) -> dict[str, Any]:
"""
Получает информацию о трафике пользователя по client_id.
Args:
xui: Экземпляр API клиента
client_id: UUID клиента
Returns:
dict[str, Any]: Информация о трафике пользователя или ошибка
"""
try:
await xui.login()
traffic_data = await xui.client.get_traffic_by_id(client_id)
if not traffic_data:
logger.warning(f"Трафик для клиента {client_id} не найден.")
return {"status": "not_found", "client_id": client_id}
logger.info(f"Трафик для клиента {client_id} успешно получен.")
return {"status": "success", "client_id": client_id, "traffic": traffic_data}
except Exception as e:
logger.error(f"Ошибка при получении трафика клиента {client_id}: {e}")
return {"status": "error", "error": str(e)}
+4
View File
@@ -275,6 +275,7 @@ async def handle_servers_availability(
await callback_query.message.edit_text(text=text)
result_text = f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n"
total_online_users = 0
for server in cluster_servers:
xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD)
@@ -282,11 +283,14 @@ async def handle_servers_availability(
try:
await xui.login()
online_users = len(await xui.client.online())
total_online_users += online_users
result_text += f"🌍 {server['server_name']}: {online_users} активных пользователей.\n"
except Exception as e:
result_text += f"{server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n"
result_text += f"\n👥 Общее количество активных пользователей в кластере: {total_online_users}."
await callback_query.message.edit_text(text=result_text, reply_markup=build_admin_back_kb("servers"))
+31 -5
View File
@@ -20,11 +20,7 @@ from database import (
update_trial,
)
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import (
delete_key_from_cluster,
renew_key_in_cluster,
update_subscription,
)
from handlers.keys.key_utils import delete_key_from_cluster, get_user_traffic, renew_key_in_cluster, update_subscription
from handlers.utils import sanitize_key_name
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from keyboards.admin.users_kb import (
@@ -673,3 +669,33 @@ async def set_user_balance(tg_id: int, balance: int, session: Any) -> None:
)
except Exception as e:
logger.error(f"Ошибка при установке баланса для пользователя {tg_id}: {e}")
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_traffic"), IsAdminFilter())
async def handle_user_traffic(
callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any
):
"""
Обработчик кнопки "📊 Трафик".
Получает трафик пользователя и отправляет администратору.
"""
tg_id = callback_data.tg_id
email = callback_data.data
await callback_query.message.edit_text("⏳ Получаем данные о трафике, пожалуйста, подождите...")
traffic_data = await get_user_traffic(session, tg_id, email)
if traffic_data["status"] == "error":
await callback_query.message.edit_text(traffic_data["message"], reply_markup=build_editor_kb(tg_id, True))
return
result_text = f"📊 Трафик ключа {email}:\n\n"
for server, traffic in traffic_data["traffic"].items():
if isinstance(traffic, str):
result_text += f"{server}: {traffic}\n"
else:
result_text += f"🌍 {server}: {traffic} ГБ\n"
await callback_query.message.edit_text(result_text, reply_markup=build_editor_kb(tg_id, True))
+68 -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
from py3xui import AsyncApi
from client import ClientConfig, add_client, delete_client, extend_client_key
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 database import get_servers, store_key
from handlers.utils import get_least_loaded_cluster
from logger import logger
@@ -277,3 +277,69 @@ async def update_subscription(tg_id: int, email: str, session: Any) -> None:
server_id=least_loaded_cluster_id,
session=session,
)
async def get_user_traffic(session: Any, tg_id: int, email: str) -> dict[str, Any]:
"""
Получает трафик пользователя на всех серверах, где у него есть ключ.
Args:
session (Any): Сессия базы данных.
tg_id (int): ID пользователя Telegram.
email (str): Email пользователя.
Returns:
dict[str, Any]: Структура с данными о трафике.
"""
logger.info(f"🔍 Получаем ключи для пользователя {email} (TG ID: {tg_id})")
query = "SELECT client_id, server_id FROM keys WHERE tg_id = $1 AND email = $2"
rows = await session.fetch(query, tg_id, email)
if not rows:
return {"status": "error", "message": "❌ У пользователя нет активных ключей."}
server_ids = {row["server_id"] for row in rows}
logger.info(f"🖥️ Серверы/Кластеры пользователя: {server_ids}")
if USE_COUNTRY_SELECTION:
query_servers = "SELECT server_name, api_url FROM servers WHERE server_name = ANY($1)"
filter_ids = list(server_ids)
else:
query_servers = "SELECT server_name, api_url FROM servers WHERE cluster_name = ANY($1)"
filter_ids = list(server_ids)
server_rows = await session.fetch(query_servers, filter_ids)
if not server_rows:
logger.error(f"❌ Не найдено серверов для: {server_ids}")
return {"status": "error", "message": f"❌ Серверы не найдены: {', '.join(server_ids)}"}
servers_map = {row["server_name"]: row["api_url"] for row in server_rows}
logger.info(f"✅ Найденные серверы: {list(servers_map.keys())}")
user_traffic_data = {}
for row in rows:
client_id = row["client_id"]
for server, api_url in servers_map.items():
if not USE_COUNTRY_SELECTION and server not in servers_map:
continue
xui = AsyncApi(api_url, username=ADMIN_USERNAME, password=ADMIN_PASSWORD)
try:
traffic_info = await get_client_traffic(xui, client_id)
if traffic_info["status"] == "success" and traffic_info["traffic"]:
client_data = traffic_info["traffic"][0]
used_gb = (client_data.up + client_data.down) / 1073741824
user_traffic_data[server] = round(used_gb, 2)
else:
user_traffic_data[server] = "Ошибка получения трафика"
except Exception as e:
user_traffic_data[server] = f"Ошибка: {e}"
return {"status": "success", "traffic": user_traffic_data}
File diff suppressed because it is too large Load Diff
+9 -4
View File
@@ -189,15 +189,20 @@ async def view_tariffs_handler(callback_query: CallbackQuery):
async def invite_handler(callback_query: CallbackQuery):
chat_id = callback_query.message.chat.id
referral_link = get_referral_link(chat_id)
referral_stats = await get_referral_stats(chat_id)
invite_text = f"\nПриглашаю тебя пользоваться действительно быстрым VPN вместе:\n\n{referral_link}"
invite_message = invite_message_send(referral_link, referral_stats)
image_path = os.path.join("img", "pic_invite.jpg")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
builder.button(
text="📢 Поделиться",
switch_inline_query=invite_text
)
builder.button(text="👤 Личный кабинет", callback_data="profile")
builder.adjust(1)
if os.path.isfile(image_path):
async with aiofiles.open(image_path, "rb") as image_file:
image_data = await image_file.read()
@@ -210,4 +215,4 @@ async def invite_handler(callback_query: CallbackQuery):
await callback_query.message.answer(
text=invite_message,
reply_markup=builder.as_markup(),
)
)
+5 -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 config import RENEWAL_PRICES
from keyboards.admin.panel_kb import build_admin_back_btn
@@ -166,6 +166,10 @@ def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup:
text="❌ Удалить",
callback_data=AdminUserEditorCallback(action="users_delete_key", data=email, tg_id=key_details["tg_id"]).pack(),
)
builder.button(
text="📊 Трафик",
callback_data=AdminUserEditorCallback(action="users_traffic", data=email, tg_id=key_details["tg_id"]).pack(),
)
builder.row(build_editor_back_btn(key_details["tg_id"], True))
builder.adjust(1)
return builder.as_markup()
+1 -1
View File
File diff suppressed because one or more lines are too long