From 4a67f9233d35e167d272aabc06f38a0db18d2809 Mon Sep 17 00:00:00 2001 From: Vladless Date: Thu, 27 Mar 2025 04:39:05 +0300 Subject: [PATCH] customizing the menu, resetting traffic, and creating a key from the admin panel --- .../admin/management/management_handler.py | 2 +- handlers/admin/users/keyboard.py | 14 +- handlers/admin/users/users_handler.py | 129 +++++++++++++++++- handlers/keys/key_utils.py | 48 +++++++ .../notifications/general_notifications.py | 14 ++ handlers/profile.py | 15 +- 6 files changed, 214 insertions(+), 8 deletions(-) diff --git a/handlers/admin/management/management_handler.py b/handlers/admin/management/management_handler.py index 0e8a5636..de44f32a 100644 --- a/handlers/admin/management/management_handler.py +++ b/handlers/admin/management/management_handler.py @@ -29,7 +29,7 @@ async def request_new_domain(callback_query: CallbackQuery, state: FSMContext): """Запрашивает у администратора новый домен.""" await state.set_state(AdminManagementStates.waiting_for_new_domain) await callback_query.message.edit_text( - text="🌐 Введите новый домен (без https://):\nПример: pocomachodomen.ru", + text="🌐 Введите новый домен (без https://):\nПример: solobotdomen.ru", ) diff --git a/handlers/admin/users/keyboard.py b/handlers/admin/users/keyboard.py index a01ba28e..bfee3fd8 100644 --- a/handlers/admin/users/keyboard.py +++ b/handlers/admin/users/keyboard.py @@ -4,7 +4,7 @@ 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, TOTAL_GB from ..panel.keyboard import build_admin_back_btn @@ -27,6 +27,11 @@ def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() current_time = datetime.now(tz=timezone.utc) + builder.button( + text="➕ Создать ключ", + callback_data=AdminUserEditorCallback(action="users_create_key", tg_id=tg_id).pack(), + ) + for record in key_records: email = record["email"] expiry = datetime.fromtimestamp(record["expiry_time"] / 1000, tz=timezone.utc) @@ -174,6 +179,13 @@ def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup: text="📊 Трафик", callback_data=AdminUserEditorCallback(action="users_traffic", data=email, tg_id=key_details["tg_id"]).pack(), ) + if TOTAL_GB > 0: + builder.button( + text="♻️ Сбросить трафик", + callback_data=AdminUserEditorCallback( + action="users_reset_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() diff --git a/handlers/admin/users/users_handler.py b/handlers/admin/users/users_handler.py index e06a5a59..dcb8df6b 100644 --- a/handlers/admin/users/users_handler.py +++ b/handlers/admin/users/users_handler.py @@ -1,4 +1,5 @@ import asyncio +import uuid from datetime import datetime, timedelta, timezone from typing import Any @@ -11,7 +12,7 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message from aiogram.utils.keyboard import InlineKeyboardBuilder -from config import TOTAL_GB +from config import TOTAL_GB, PUBLIC_LINK, RENEWAL_PRICES from database import ( delete_key, @@ -22,9 +23,10 @@ from database import ( update_balance, update_key_expiry, update_trial, + store_key ) from filters.admin import IsAdminFilter -from handlers.keys.key_utils import delete_key_from_cluster, get_user_traffic, 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, create_key_on_cluster, reset_traffic_in_cluster from handlers.utils import sanitize_key_name from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn, build_admin_back_kb from .keyboard import ( @@ -43,6 +45,7 @@ from .keyboard import ( ) from logger import logger from utils.csv_export import export_referrals_csv +from handlers.utils import generate_random_email MOSCOW_TZ = pytz.timezone("Europe/Moscow") @@ -784,3 +787,125 @@ async def handle_users_export_referrals( await callback_query.message.answer_document( document=csv_file, caption=f"Список рефералов для пользователя {referrer_tg_id}." ) + + +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_create_key"), IsAdminFilter()) +async def handle_create_key_select_cluster(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any): + tg_id = callback_data.tg_id + await state.update_data(tg_id=tg_id) + + servers = await get_servers(session) + cluster_names = list(servers.keys()) + + if not cluster_names: + await callback_query.message.edit_text("❌ Нет доступных кластеров для создания ключа.", reply_markup=build_editor_kb(tg_id)) + return + + builder = InlineKeyboardBuilder() + for cluster in cluster_names: + builder.button( + text=f"🌐 {cluster}", + callback_data=AdminUserEditorCallback(action="users_create_key_cluster", tg_id=tg_id, data=cluster).pack(), + ) + builder.row(build_admin_back_btn()) + + await callback_query.message.edit_text( + "🌐 Выберите кластер для создания ключа:", + reply_markup=builder.as_markup() + ) + + +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_create_key_cluster"), IsAdminFilter()) +async def handle_create_key_cluster(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext): + tg_id = callback_data.tg_id + cluster_name = callback_data.data + + await state.update_data(tg_id=tg_id, cluster_name=cluster_name) + + builder = InlineKeyboardBuilder() + for months, _ in RENEWAL_PRICES.items(): + builder.button( + text=f"{months} мес.", + callback_data=f"create_key_duration|{tg_id}|{cluster_name}|{months}" + ) + builder.adjust(1) + builder.row(build_admin_back_btn()) + + await callback_query.message.edit_text( + text=f"🕒 Выберите срок действия ключа для кластера {cluster_name}:", + reply_markup=builder.as_markup() + ) + + +@router.callback_query(F.data.startswith("create_key_duration|"), IsAdminFilter()) +async def handle_create_key_duration(callback_query: CallbackQuery, session: Any): + try: + parts = callback_query.data.split("|") + tg_id = int(parts[1]) + cluster_name = parts[2] + months = int(parts[3]) + + client_id = str(uuid.uuid4()) + email = generate_random_email() + expiry = datetime.now(tz=timezone.utc) + timedelta(days=30 * months) + expiry_ms = int(expiry.timestamp() * 1000) + + await create_key_on_cluster(cluster_name, tg_id, client_id, email, expiry_ms) + + public_link = f"{PUBLIC_LINK}{email}/{tg_id}" + + await store_key( + tg_id=tg_id, + client_id=client_id, + email=email, + expiry_time=expiry_ms, + key=public_link, + server_id=cluster_name, + session=session, + ) + + await callback_query.message.edit_text( + f"✅ Ключ успешно создан в кластере {cluster_name} на {months} мес.!", + reply_markup=build_editor_kb(tg_id) + ) + + except Exception as e: + logger.error(f"Ошибка при создании ключа: {e}") + await callback_query.message.edit_text( + "❌ Не удалось создать ключ. Попробуйте позже.", + reply_markup=build_editor_kb(tg_id) + ) + + +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_reset_traffic"), IsAdminFilter()) +async def handle_reset_traffic(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any): + tg_id = callback_data.tg_id + email = callback_data.data + + record = await session.fetchrow( + "SELECT server_id, client_id FROM keys WHERE tg_id = $1 AND email = $2", + tg_id, + email, + ) + + if not record: + await callback_query.message.edit_text( + "❌ Ключ не найден в базе данных.", + reply_markup=build_editor_kb(tg_id) + ) + return + + cluster_id = record["server_id"] + + try: + await reset_traffic_in_cluster(cluster_id, email) + await callback_query.message.edit_text( + f"✅ Трафик для ключа {email} успешно сброшен.", + reply_markup=build_editor_kb(tg_id) + ) + except Exception as e: + logger.error(f"Ошибка при сбросе трафика: {e}") + await callback_query.message.edit_text( + "❌ Произошла ошибка при сбросе трафика. Попробуйте позже.", + reply_markup=build_editor_kb(tg_id) + ) \ No newline at end of file diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index d501bf8c..b86d95ab 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -480,3 +480,51 @@ async def toggle_client_on_cluster(cluster_id: str, email: str, client_id: str, except Exception as e: logger.error(f"Ошибка при изменении состояния клиента {email} в кластере {cluster_id}: {e}") return {"status": "error", "error": str(e)} + + +async def reset_traffic_in_cluster(cluster_id: str, email: str) -> None: + """ + Сбрасывает трафик клиента на всех серверах указанного кластера (или конкретного сервера). + + Args: + cluster_id (str): ID кластера или имя сервера + email (str): Email клиента (будет преобразован в уникальный для SUPERNODE) + """ + 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} не найден.") + + tasks = [] + for server_info in cluster: + api_url = server_info["api_url"] + 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}. Пропуск.") + continue + + xui = AsyncApi(api_url, username=ADMIN_USERNAME, password=ADMIN_PASSWORD, logger=logger) + + unique_email = f"{email}_{server_name.lower()}" if SUPERNODE else email + + tasks.append(xui.client.reset_stats(int(inbound_id), unique_email)) + + await asyncio.gather(*tasks, return_exceptions=True) + logger.info(f"✅ Трафик клиента {email} успешно сброшен на всех серверах кластера {cluster_id}") + + except Exception as e: + logger.error(f"❌ Ошибка при сбросе трафика клиента {email} в кластере {cluster_id}: {e}") + raise diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py index bd51acf6..c882398b 100644 --- a/handlers/notifications/general_notifications.py +++ b/handlers/notifications/general_notifications.py @@ -258,6 +258,20 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: delete_immediately = NOTIFY_DELETE_DELAY == 0 delete_after_delay = False + if NOTIFY_DELETE_DELAY > 0 and last_notification_time is not None: + minutes_since = (current_time - last_notification_time) / (1000 * 60) + if minutes_since >= NOTIFY_DELETE_DELAY / 2 and minutes_since < NOTIFY_DELETE_DELAY: + try: + await conn.execute( + "DELETE FROM notifications WHERE tg_id = $1 AND notification_type = $2", + tg_id, + notification_id, + ) + logger.info(f"⛔ Уведомление {notification_id} для {tg_id} удалено (прошло больше половины задержки).") + except Exception as e: + logger.error(f"Ошибка при удалении уведомления: {e}") + continue + if last_notification_time is not None: delete_after_delay = (current_time - last_notification_time) / (1000 * 60) >= NOTIFY_DELETE_DELAY logger.info( diff --git a/handlers/profile.py b/handlers/profile.py index 17c8f8bc..c9268df9 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -25,6 +25,8 @@ from config import ( RENEWAL_PLANS, TRIAL_TIME, USERNAME_BOT, + REFERRAL_BUTTON, + GIFT_BUTTON ) from database import get_balance, get_key_count, get_last_payments, get_referral_stats, get_trial from handlers.buttons.profile import ( @@ -87,10 +89,15 @@ async def process_callback_view_profile( else: builder.row(InlineKeyboardButton(text=MY_SUBS, callback_data="view_keys")) builder.row(InlineKeyboardButton(text=BALANCE, callback_data="balance")) - builder.row( - InlineKeyboardButton(text=INVITE, callback_data="invite"), - InlineKeyboardButton(text=GIFTS, callback_data="gifts"), - ) + + row_buttons = [] + if REFERRAL_BUTTON: + row_buttons.append(InlineKeyboardButton(text=INVITE, callback_data="invite")) + if GIFT_BUTTON: + row_buttons.append(InlineKeyboardButton(text=GIFTS, callback_data="gifts")) + if row_buttons: + builder.row(*row_buttons) + if INSTRUCTIONS_BUTTON: builder.row(InlineKeyboardButton(text=INSTRUCTIONS, callback_data="instructions")) if admin: