From e13937b965cc0ee4c966ced6ef9a84f37113959d Mon Sep 17 00:00:00 2001 From: hteppl Date: Thu, 9 Jan 2025 15:13:36 +0300 Subject: [PATCH 01/23] Introducing keyboards package --- keyboards/__init__.py | 0 keyboards/admin/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 keyboards/__init__.py create mode 100644 keyboards/admin/__init__.py diff --git a/keyboards/__init__.py b/keyboards/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keyboards/admin/__init__.py b/keyboards/admin/__init__.py new file mode 100644 index 00000000..e69de29b From ba53e9e199a1edecc602523a65175911fa09f33f Mon Sep 17 00:00:00 2001 From: hteppl Date: Thu, 9 Jan 2025 15:43:22 +0300 Subject: [PATCH 02/23] Update admin main panel --- handlers/admin/admin_panel.py | 542 ++-------------------------------- keyboards/admin/panel_kb.py | 80 +++++ 2 files changed, 99 insertions(+), 523 deletions(-) create mode 100644 keyboards/admin/panel_kb.py diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index 88a69282..3df31a34 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -1,544 +1,40 @@ -import subprocess -from datetime import datetime -from io import BytesIO -from typing import Any - -import asyncpg from aiogram import F, Router, types from aiogram.filters import Command from aiogram.fsm.context import FSMContext -from aiogram.fsm.state import State, StatesGroup -from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton -from aiogram.utils.keyboard import InlineKeyboardBuilder +from aiogram.types import CallbackQuery -from backup import backup_database -from bot import bot -from config import DATABASE_URL from filters.admin import IsAdminFilter -from logger import logger -from database import delete_user_data +from keyboards.admin.panel_kb import build_panel_kb, AdminPanelCallback, build_management_kb router = Router() -class UserEditorState(StatesGroup): - waiting_for_tg_id = State() - displaying_user_info = State() - waiting_for_restart_confirmation = State() - waiting_for_message = State() - - -@router.callback_query(F.data == "admin", IsAdminFilter()) +@router.callback_query( + F.data == "admin", + IsAdminFilter() +) async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext): await handle_admin_message(callback_query.message, state) -@router.message(Command("admin"), F.data == "admin", IsAdminFilter()) +@router.message( + Command("admin"), + IsAdminFilter() +) async def handle_admin_message(message: types.Message, state: FSMContext): await state.clear() - - BOT_VERSION = "4.0.0-preAlpha" # Укажите текущую версию бота - - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="📊 Статистика пользователей", callback_data="user_stats" - ) - ) - builder.row( - InlineKeyboardButton( - text="👥 Управление пользователями", callback_data="user_editor" - ) - ) - builder.row( - InlineKeyboardButton( - text="🖥️ Управление серверами", callback_data="servers_editor" - ) - ) - builder.row( - InlineKeyboardButton( - text="🎟️ Управление купонами", callback_data="coupons_editor" - ) - ) - builder.row( - InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to") - ) - builder.row( - InlineKeyboardButton( - text="🤖 Управление Ботом", callback_data="bot_management" - ) - ) - builder.row( - InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") - ) - await message.answer( - f"🤖 Панель администратора\n\nВерсия бота: {BOT_VERSION}", - reply_markup=builder.as_markup(), - parse_mode="HTML" - ) - - -@router.callback_query(F.data == "bot_management") -async def handle_bot_management(callback_query: types.CallbackQuery): - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups") - ) - builder.row( - InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot") - ) - builder.row( - InlineKeyboardButton(text="🚫 Баны", callback_data="ban_user") - ) - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data="admin") - ) - await callback_query.message.answer( - "🤖 Управление ботом", - reply_markup=builder.as_markup(), - ) - - - -@router.callback_query(F.data == "user_stats", IsAdminFilter()) -async def user_stats_menu(callback_query: CallbackQuery, session: Any): - try: - total_users = await session.fetchval("SELECT COUNT(*) FROM connections") - total_keys = await session.fetchval("SELECT COUNT(*) FROM keys") - total_referrals = await session.fetchval("SELECT COUNT(*) FROM referrals") - - total_payments_today = await session.fetchval( - "SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= CURRENT_DATE" - ) - total_payments_week = await session.fetchval( - "SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('week', CURRENT_DATE)" - ) - total_payments_month = await session.fetchval( - "SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('month', CURRENT_DATE)" - ) - total_payments_all_time = await session.fetchval( - "SELECT COALESCE(SUM(amount), 0) FROM payments" - ) - - active_keys = await session.fetchval( - "SELECT COUNT(*) FROM keys WHERE expiry_time > $1", - int(datetime.utcnow().timestamp() * 1000), - ) - expired_keys = total_keys - active_keys - - stats_message = ( - f"📊 Подробная статистика проекта:\n\n" - f"👥 Пользователи:\n" - f" 🌐 Зарегистрировано: {total_users}\n" - f" 🤝 Привлеченных рефералов: {total_referrals}\n\n" - f"🔑 Ключи:\n" - f" 🌈 Всего сгенерировано: {total_keys}\n" - f" ✅ Действующих: {active_keys}\n" - f" ❌ Просроченных: {expired_keys}\n\n" - f"💰 Финансовая статистика:\n" - f" 📅 За день: {total_payments_today} ₽\n" - f" 📆 За неделю: {total_payments_week} ₽\n" - f" 📆 За месяц: {total_payments_month} ₽\n" - f" 🏦 За все время: {total_payments_all_time} ₽\n" - ) - - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="🔄 Обновить", callback_data="user_stats") - ) - builder.row( - InlineKeyboardButton( - text="📥 Выгрузить пользователей в CSV", - callback_data="export_users_csv", - ) - ) - builder.row( - InlineKeyboardButton( - text="📥 Выгрузить оплаты в CSV", callback_data="export_payments_csv" - ) - ) - builder.row( - InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin") - ) - - await callback_query.message.answer( - stats_message, reply_markup=builder.as_markup() - ) - except Exception as e: - logger.error(f"Error in user_stats_menu: {e}") - - -@router.callback_query(F.data == "export_users_csv", IsAdminFilter()) -async def export_users_csv(callback_query: CallbackQuery, session: Any): - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_stats")) - try: - users = await session.fetch( - """ - SELECT - u.tg_id, - u.username, - u.first_name, - u.last_name, - u.language_code, - u.is_bot, - c.balance, - c.trial - FROM users u - LEFT JOIN connections c ON u.tg_id = c.tg_id - """ - ) - - if not users: - await callback_query.message.answer( - "📭 Нет пользователей для экспорта.", reply_markup=builder.as_markup() - ) - return - - csv_data = "tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n" - for user in users: - csv_data += f"{user['tg_id']},{user['username']},{user['first_name']},{user['last_name']},{user['language_code']},{user['is_bot']},{user['balance']},{user['trial']}\n" - - file_name = BytesIO(csv_data.encode("utf-8-sig")) - file_name.seek(0) - - file = BufferedInputFile(file_name.getvalue(), filename="users_export.csv") - - await callback_query.message.answer_document( - file, - caption="📥 Экспорт пользователей в CSV", - reply_markup=builder.as_markup(), - ) - file_name.close() - - except Exception as e: - logger.error(f"Ошибка при экспорте пользователей в CSV: {e}") - await callback_query.message.answer( - "❗ Произошла ошибка при экспорте пользователей.", - reply_markup=builder.as_markup(), - ) - - -@router.callback_query(F.data == "export_payments_csv", IsAdminFilter()) -async def export_payments_csv(callback_query: CallbackQuery, session: Any): - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_stats")) - try: - payments = await session.fetch( - """ - SELECT - u.tg_id, - u.username, - u.first_name, - u.last_name, - p.amount, - p.payment_system, - p.status, - p.created_at - FROM users u - JOIN payments p ON u.tg_id = p.tg_id - """ - ) - - if not payments: - await callback_query.message.answer( - "📭 Нет платежей для экспорта.", reply_markup=builder.as_markup() - ) - return - - csv_data = "tg_id,username,first_name,last_name,amount,payment_system,status,created_at\n" # Заголовки CSV - for payment in payments: - csv_data += f"{payment['tg_id']},{payment['username']},{payment['first_name']},{payment['last_name']},{payment['amount']},{payment['payment_system']},{payment['status']},{payment['created_at']}\n" - - file_name = BytesIO(csv_data.encode("utf-8-sig")) - file_name.seek(0) - - file = BufferedInputFile(file_name.getvalue(), filename="payments_export.csv") - - await callback_query.message.answer_document( - file, caption="📥 Экспорт платежей в CSV", reply_markup=builder.as_markup() - ) - file_name.close() - - except Exception as e: - logger.error(f"Ошибка при экспорте платежей в CSV: {e}") - await callback_query.message.answer( - "❗ Произошла ошибка при экспорте платежей.", - reply_markup=builder.as_markup(), - ) - - -@router.callback_query(F.data == "send_to", IsAdminFilter()) -async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext): - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="📢 Отправить всем", callback_data="send_to_all")) - builder.row(InlineKeyboardButton(text="📢 Отправить с подпиской", callback_data="send_to_subscribed")) - builder.row(InlineKeyboardButton(text="📢 Отправить без подписки", callback_data="send_to_unsubscribed")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) - await callback_query.message.answer( - "✍️ Выберите группу пользователей и введите текст сообщения для рассылки:", - reply_markup=builder.as_markup(), - ) - -@router.callback_query(F.data == "send_to_all", IsAdminFilter()) -async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext): - await state.update_data(send_to="all") - await callback_query.message.answer( - "✍️ Введите текст сообщения для рассылки всем пользователям:" - ) - await state.set_state(UserEditorState.waiting_for_message) - -@router.callback_query(F.data == "send_to_subscribed", IsAdminFilter()) -async def handle_send_to_subscribed(callback_query: CallbackQuery, state: FSMContext): - await state.update_data(send_to="subscribed") - await callback_query.message.answer( - "✍️ Введите текст сообщения для рассылки пользователям с активной подпиской:" - ) - await state.set_state(UserEditorState.waiting_for_message) - -@router.callback_query(F.data == "send_to_unsubscribed", IsAdminFilter()) -async def handle_send_to_unsubscribed(callback_query: CallbackQuery, state: FSMContext): - await state.update_data(send_to="unsubscribed") - await callback_query.message.answer( - "✍️ Введите текст сообщения для рассылки пользователям без активной подписки:" - ) - await state.set_state(UserEditorState.waiting_for_message) - - -@router.message(UserEditorState.waiting_for_message, IsAdminFilter()) -async def process_message_to_all( - message: types.Message, state: FSMContext, session: Any -): - text_message = message.text - - try: - state_data = await state.get_data() - send_to = state_data.get('send_to', 'all') - - if send_to == 'all': - tg_ids = await session.fetch("SELECT DISTINCT tg_id FROM connections") - elif send_to == 'subscribed': - tg_ids = await session.fetch(""" - SELECT DISTINCT c.tg_id - FROM connections c - JOIN keys k ON c.tg_id = k.tg_id - WHERE k.expiry_time > $1 - """, int(datetime.utcnow().timestamp() * 1000)) - elif send_to == 'unsubscribed': - tg_ids = await session.fetch(""" - SELECT c.tg_id - FROM connections c - LEFT JOIN keys k ON c.tg_id = k.tg_id - GROUP BY c.tg_id - HAVING COUNT(k.tg_id) = 0 OR MAX(k.expiry_time) <= $1 - """, int(datetime.utcnow().timestamp() * 1000)) - - total_users = len(tg_ids) - success_count = 0 - error_count = 0 - - for record in tg_ids: - tg_id = record["tg_id"] - try: - await bot.send_message(chat_id=tg_id, text=text_message) - success_count += 1 - except Exception as e: - error_count += 1 - logger.error( - f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}" - ) - - await message.answer( - f"📤 Рассылка завершена:\n" - f"👥 Всего пользователей: {total_users}\n" - f"✅ Успешно отправлено: {success_count}\n" - f"❌ Не доставлено: {error_count}" - ) - except Exception as e: - logger.error(f"❗ Ошибка при подключении к базе данных: {e}") - - await state.clear() - - -@router.callback_query(F.data == "backups", IsAdminFilter()) -async def handle_backup(callback_query: CallbackQuery, state: FSMContext): - await callback_query.message.answer( - "💾 Инициализация резервного копирования базы данных..." - ) - await backup_database() - await callback_query.message.answer( - "✅ Резервная копия успешно создана и отправлена администратору." - ) - - -@router.callback_query(F.data == "restart_bot", IsAdminFilter()) -async def handle_restart(callback_query: CallbackQuery, state: FSMContext): - await state.set_state(UserEditorState.waiting_for_restart_confirmation) - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="✅ Да, перезапустить", callback_data="confirm_restart" - ), - InlineKeyboardButton(text="❌ Нет, отмена", callback_data="admin"), - ) - builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin")) - await callback_query.message.answer( - "🤔 Вы уверены, что хотите перезапустить бота?", - reply_markup=builder.as_markup(), + await message.message.answer( + text="🤖 Панель администратора", + reply_markup=build_panel_kb() ) @router.callback_query( - F.data == "confirm_restart", - UserEditorState.waiting_for_restart_confirmation, - IsAdminFilter(), + AdminPanelCallback.filter(F.action == "management"), + IsAdminFilter() ) -async def confirm_restart_bot(callback_query: CallbackQuery, state: FSMContext): - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin")) - try: - subprocess.run( - ["sudo", "systemctl", "restart", "bot.service"], - check=True, - capture_output=True, - text=True, - ) - await state.clear() - await callback_query.message.answer( - "🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup() - ) - except subprocess.CalledProcessError: - await callback_query.message.answer( - "🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup() - ) - except Exception as e: - await callback_query.message.answer( - f"⚠️ Ошибка при перезагрузке бота: {e.stderr}", - reply_markup=builder.as_markup(), - ) - - -@router.callback_query(F.data == "user_editor", IsAdminFilter()) -async def user_editor_menu(callback_query: CallbackQuery): - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔍 Поиск по названию ключа", - callback_data="search_by_key_name", - ) +async def handle_restart(callback_query: CallbackQuery): + await callback_query.message.edit_text( + text="🤖 Управление ботом", + reply_markup=build_management_kb(), ) - builder.row( - InlineKeyboardButton( - text="🆔 Поиск по Telegram ID", callback_data="search_by_tg_id" - ) - ) - builder.row( - InlineKeyboardButton( - text="🌐 Поиск по Username", callback_data="search_by_username" - ) - ) - builder.row(InlineKeyboardButton(text="🔙 Вернуться назад", callback_data="admin")) - await callback_query.message.answer( - "👇 Выберите способ поиска пользователя:", reply_markup=builder.as_markup() - ) - - -@router.callback_query(F.data == "ban_user") -async def handle_ban_user(callback_query: types.CallbackQuery): - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="📄 Выгрузить в CSV", callback_data="export_to_csv") - ) - builder.row( - InlineKeyboardButton(text="🗑️ Удалить из БД", callback_data="delete_banned_users") - ) - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management") - ) - await callback_query.message.answer( - "🚫 Заблокировавшие бота\n\n" - "Здесь можно просматривать и удалять пользователей, которые забанили вашего бота!", - reply_markup=builder.as_markup(), - ) - - -@router.callback_query(F.data == "export_to_csv") -async def export_banned_users_to_csv(callback_query: types.CallbackQuery): - conn = await asyncpg.connect(DATABASE_URL) - try: - banned_users = await conn.fetch("SELECT tg_id, blocked_at FROM blocked_users") - - import csv - import io - csv_output = io.StringIO() - writer = csv.writer(csv_output) - writer.writerow(["tg_id", "blocked_at"]) - for user in banned_users: - writer.writerow([user["tg_id"], user["blocked_at"]]) - - csv_output.seek(0) - - document = BufferedInputFile( - file=csv_output.getvalue().encode("utf-8"), - filename="banned_users.csv" - ) - - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management") - ) - - await callback_query.message.answer_document( - document=document, - caption="📄 Список заблокировавших бота пользователей", - reply_markup=builder.as_markup(), - ) - except Exception as e: - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management") - ) - await callback_query.message.answer( - text=f"Ошибка при выгрузке CSV: {e}", - reply_markup=builder.as_markup(), - ) - finally: - await conn.close() - - -@router.callback_query(F.data == "delete_banned_users") -async def delete_banned_users(callback_query: types.CallbackQuery): - conn = await asyncpg.connect(DATABASE_URL) - try: - blocked_users = await conn.fetch("SELECT tg_id FROM blocked_users") - blocked_ids = [record['tg_id'] for record in blocked_users] - - if not blocked_ids: - await callback_query.message.answer("📂 Нет заблокировавших пользователей для удаления.") - return - - for tg_id in blocked_ids: - await delete_user_data(conn, tg_id) - - await conn.execute("DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids) - - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management") - ) - await callback_query.message.answer( - text=f"🗑️ Удалено данные о {len(blocked_ids)} пользователях и связанных записях.", - reply_markup=builder.as_markup(), - ) - except Exception as e: - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management") - ) - await callback_query.message.answer( - text=f"Ошибка при удалении записей: {e}", - reply_markup=builder.as_markup(), - ) - finally: - await conn.close() - diff --git a/keyboards/admin/panel_kb.py b/keyboards/admin/panel_kb.py new file mode 100644 index 00000000..3c517d48 --- /dev/null +++ b/keyboards/admin/panel_kb.py @@ -0,0 +1,80 @@ +from aiogram.filters.callback_data import CallbackData +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder + + +class AdminPanelCallback(CallbackData, prefix='admin_panel'): + action: str + + +def build_panel_kb() -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button( + text="🔍 Поиск пользователя", + callback_data=AdminPanelCallback(action="search_user").pack() + ) + builder.button( + text="🔑 Поиск по названию ключа", + callback_data=AdminPanelCallback(action="search_key").pack() + ) + builder.row( + InlineKeyboardButton( + text="🖥️ Серверы", + callback_data=AdminPanelCallback(action="servers").pack() + ), + InlineKeyboardButton( + text="🎟️ Купоны", + callback_data=AdminPanelCallback(action="coupons").pack() + ) + ) + builder.button( + text="📢 Рассылка", + callback_data=AdminPanelCallback(action="sender").pack() + ) + builder.button( + text="🤖 Управление Ботом", + callback_data=AdminPanelCallback(action="management").pack() + ) + builder.button( + text="📊 Статистика", + callback_data=AdminPanelCallback(action="stats").pack() + ) + builder.button( + text="👤 Личный кабинет", + callback_data="profile" + ) + return builder.as_markup() + + +def build_management_kb() -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button( + text="💾 Создать резервную копию", + callback_data=AdminPanelCallback(action="backups").pack() + ) + builder.button( + text="🚫 Баны", + callback_data=AdminPanelCallback(action="bans").pack() + ) + builder.button( + text="🔄 Перезагрузить бота", + callback_data=AdminPanelCallback(action="restart").pack() + ) + builder.button( + text="⬅️ Назад", + callback_data="admin" + ) + return builder.as_markup() + + +def build_restart_kb() -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button( + text="✅ Да, перезапустить", + callback_data=AdminPanelCallback(action="restart_confirm").pack() + ) + builder.button( + text="🔙 Назад", + callback_data="admin" + ) + return builder.as_markup() From 73bf682f9779d926a01f28bb9b3ed05603f31f50 Mon Sep 17 00:00:00 2001 From: hteppl Date: Thu, 9 Jan 2025 15:48:32 +0300 Subject: [PATCH 03/23] Update admin coupons handler --- handlers/admin/admin_coupons.py | 228 +++++++++++++++++--------------- keyboards/admin/coupons_kb.py | 43 ++++++ keyboards/admin/panel_kb.py | 10 ++ 3 files changed, 176 insertions(+), 105 deletions(-) create mode 100644 keyboards/admin/coupons_kb.py diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index 27e3071e..ddcbf67b 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -3,56 +3,141 @@ from typing import Any from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import InlineKeyboardButton -from aiogram.utils.keyboard import InlineKeyboardBuilder -from database import create_coupon, delete_coupon_from_db, get_all_coupons +from database import delete_coupon_from_db, get_all_coupons, create_coupon from filters.admin import IsAdminFilter +from keyboards.admin.coupons_kb import build_coupons_kb, build_coupons_list_kb, AdminCouponDeleteCallback +from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb from logger import logger +router = Router() + class AdminCouponsState(StatesGroup): waiting_for_coupon_data = State() -router = Router() - - -@router.callback_query(F.data == "coupons_editor", IsAdminFilter()) +@router.callback_query( + AdminPanelCallback.filter(F.action == "coupons"), + IsAdminFilter(), +) async def show_coupon_management_menu( - callback_query: types.CallbackQuery, state: FSMContext + callback_query: types.CallbackQuery, + state: FSMContext ): await state.clear() - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="➕ Создать купон", callback_data="create_coupon") - ) - builder.row(InlineKeyboardButton(text="Купоны", callback_data="coupons")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) + + kb = build_coupons_kb() + await callback_query.message.answer( - "🛠 Меню управления купонами:", reply_markup=builder.as_markup() + text="🛠 Меню управления купонами:", + reply_markup=kb ) -@router.callback_query(F.data == "coupons", IsAdminFilter()) -async def show_coupon_list(callback_query: types.CallbackQuery, session: Any): +@router.callback_query( + AdminPanelCallback.filter(F.action == "coupons_create"), + IsAdminFilter(), +) +async def handle_create_coupon( + callback_query: types.CallbackQuery, + state: FSMContext +): + text = ( + "🎫 Введите данные для создания купона в формате:\n\n" + "📝 код 💰 сумма 🔢 лимит\n\n" + "Пример: 'COUPON1 50 5' 👈\n\n" + ) + + await callback_query.message.answer( + text=text, + reply_markup=build_admin_back_kb("coupons"), + ) + await state.set_state(AdminCouponsState.waiting_for_coupon_data) + + +@router.message( + AdminCouponsState.waiting_for_coupon_data, + IsAdminFilter() +) +async def process_coupon_data( + message: types.Message, + state: FSMContext, + session: Any +): + text = message.text.strip() + parts = text.split() + + kb = build_admin_back_kb("coupons") + + if len(parts) != 3: + text = ( + "❌ Некорректный формат! 📝 Пожалуйста, введите данные в формате:\n" + "🏷️ код 💰 сумма 🔢 лимит\n" + "Пример: 'COUPON1 50 5' 👈" + ) + + await message.answer( + text=text, + reply_markup=kb, + ) + return + + try: + coupon_code = parts[0] + coupon_amount = float(parts[1]) + usage_limit = int(parts[2]) + except ValueError: + text = ( + "⚠️ Проверьте правильность введенных данных!\n" + "💱 Сумма должна быть числом, а лимит — целым числом." + ) + + await message.answer( + text=text, + reply_markup=kb, + ) + return + + try: + await create_coupon(coupon_code, coupon_amount, usage_limit, session) + + result_message = ( + f"✅ Купон с кодом {coupon_code} успешно создан!\n" + f"💰 Сумма: {coupon_amount} рублей \n" + f"🔢 Лимит использования: {usage_limit} раз" + ) + + await message.answer( + text=result_message, + reply_markup=kb + ) + await state.clear() + + except Exception as e: + logger.error(f"Ошибка при создании купона: {e}") + + +@router.callback_query( + AdminPanelCallback.filter(F.action == "coupons_list"), + IsAdminFilter(), +) +async def show_coupon_list( + callback_query: types.CallbackQuery, + session: Any +): try: coupons = await get_all_coupons(session) if not coupons: - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor") - ) - await callback_query.message.answer( - "❌ На данный момент нет доступных купонов. 🚫\nВы можете вернуться в меню управления. 🔙", - reply_markup=builder.as_markup(), + text="❌ На данный момент нет доступных купонов. 🚫\nВы можете вернуться в меню управления. 🔙", + reply_markup=build_admin_back_kb("coupons"), ) return + kb = build_coupons_list_kb(coupons) coupon_list = "📜 Список всех купонов:\n\n" - builder = InlineKeyboardBuilder() for coupon in coupons: coupon_list += ( @@ -62,27 +147,25 @@ async def show_coupon_list(callback_query: types.CallbackQuery, session: Any): f"✅ Использовано: {coupon['usage_count']} раз\n\n" ) - builder.row( - InlineKeyboardButton( - text=f"❌ Удалить {coupon['code']}", - callback_data=f"delete_coupon_{coupon['code']}", - ) - ) - - builder.row( - InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor") - ) await callback_query.message.answer( - coupon_list, reply_markup=builder.as_markup() + text=coupon_list, + reply_markup=kb ) except Exception as e: logger.error(f"Ошибка при получении списка купонов: {e}") -@router.callback_query(F.data.startswith("delete_coupon_"), IsAdminFilter()) -async def handle_delete_coupon(callback_query: types.CallbackQuery, session: Any): - coupon_code = callback_query.data[len("delete_coupon_") :] +@router.callback_query( + AdminCouponDeleteCallback.filter(), + IsAdminFilter(), +) +async def handle_delete_coupon( + callback_query: types.CallbackQuery, + callback_data: AdminCouponDeleteCallback, + session: Any +): + coupon_code = callback_data.coupon_code try: result = await delete_coupon_from_db(coupon_code, session) @@ -91,74 +174,9 @@ async def handle_delete_coupon(callback_query: types.CallbackQuery, session: Any await show_coupon_list(callback_query, session) else: await callback_query.message.answer( - f"❌ Купон с кодом {coupon_code} не найден.", + text=f"❌ Купон с кодом {coupon_code} не найден.", ) await show_coupon_list(callback_query, session) except Exception as e: logger.error(f"Ошибка при удалении купона: {e}") - - -@router.callback_query(F.data == "create_coupon", IsAdminFilter()) -async def handle_create_coupon(callback_query: types.CallbackQuery, state: FSMContext): - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) - - await callback_query.message.answer( - "🎫 Введите данные для создания купона в формате:\n\n" - "📝 код 💰 сумма 🔢 лимит\n\n" - "Пример: 'COUPON1 50 5' 👈\n\n", - reply_markup=builder.as_markup(), - ) - await state.set_state(AdminCouponsState.waiting_for_coupon_data) - - -@router.message(AdminCouponsState.waiting_for_coupon_data, IsAdminFilter()) -async def process_coupon_data(message: types.Message, state: FSMContext, session: Any): - text = message.text.strip() - - parts = text.split() - - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) - - if len(parts) != 3: - await message.answer( - "❌ Некорректный формат! 📝 Пожалуйста, введите данные в формате:\n" - "🏷️ код 💰 сумма 🔢 лимит\n" - "Пример: 'COUPON1 50 5' 👈", - reply_markup=builder.as_markup(), - ) - return - - try: - coupon_code = parts[0] - coupon_amount = float(parts[1]) - usage_limit = int(parts[2]) - except ValueError: - await message.answer( - "⚠️ Проверьте правильность введенных данных!\n" - "💱 Сумма должна быть числом, 🔢 а лимит — целым числом.", - reply_markup=builder.as_markup(), - ) - return - - try: - await create_coupon(coupon_code, coupon_amount, usage_limit, session) - - result_message = ( - f"✅ Купон с кодом {coupon_code} успешно создан! 🎉\n" - f"Сумма: {coupon_amount} рублей 💰\n" - f"Лимит использования: {usage_limit} раз 🔢." - ) - - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor") - ) - - await message.answer(result_message, reply_markup=builder.as_markup()) - await state.clear() - - except Exception as e: - logger.error(f"Ошибка при создании купона: {e}") diff --git a/keyboards/admin/coupons_kb.py b/keyboards/admin/coupons_kb.py new file mode 100644 index 00000000..25467a16 --- /dev/null +++ b/keyboards/admin/coupons_kb.py @@ -0,0 +1,43 @@ +from aiogram.filters.callback_data import CallbackData +from aiogram.types import InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from keyboards.admin.panel_kb import AdminPanelCallback + + +class AdminCouponDeleteCallback(CallbackData, prefix='admin_coupon_delete'): + coupon_code: str + + +def build_coupons_kb() -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button( + text="➕ Создать купон", + callback_data=AdminPanelCallback(action="coupons_create").pack() + ) + builder.button( + text="Купоны", + callback_data=AdminPanelCallback(action="coupons_list").pack() + ) + builder.button( + text="🔙 Назад", + callback_data="admin" + ) + return builder.as_markup() + + +def build_coupons_list_kb(coupons: list) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + for coupon in coupons: + coupon_code = coupon["code"] + builder.button( + text=f"❌ Удалить {coupon_code}", + callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code), + ) + + builder.button( + text="🔙 Назад", + callback_data=AdminPanelCallback(action="coupons").pack() + ) + return builder.as_markup() diff --git a/keyboards/admin/panel_kb.py b/keyboards/admin/panel_kb.py index 3c517d48..b8e9133c 100644 --- a/keyboards/admin/panel_kb.py +++ b/keyboards/admin/panel_kb.py @@ -78,3 +78,13 @@ def build_restart_kb() -> InlineKeyboardMarkup: callback_data="admin" ) return builder.as_markup() + + +def build_admin_back_kb(action: str) -> InlineKeyboardMarkup: + return build_admin_singleton_kb("🔙 Назад", action) + + +def build_admin_singleton_kb(text: str, action: str) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button(text=text, callback_data=AdminPanelCallback(action=action).pack()) + return builder.as_markup() From 8a15d0320cfeb5dc504feedc7824e1c2b3dafe6f Mon Sep 17 00:00:00 2001 From: hteppl Date: Thu, 9 Jan 2025 15:49:23 +0300 Subject: [PATCH 04/23] Implement admin backups handler --- handlers/admin/admin_backups.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 handlers/admin/admin_backups.py diff --git a/handlers/admin/admin_backups.py b/handlers/admin/admin_backups.py new file mode 100644 index 00000000..d287659b --- /dev/null +++ b/handlers/admin/admin_backups.py @@ -0,0 +1,22 @@ +from aiogram import F, Router +from aiogram.types import CallbackQuery + +from backup import backup_database +from filters.admin import IsAdminFilter +from keyboards.admin.panel_kb import AdminPanelCallback + +router = Router() + + +@router.callback_query( + AdminPanelCallback.filter(F.action == "backups"), + IsAdminFilter(), +) +async def handle_backup(callback_query: CallbackQuery): + await callback_query.message.answer( + text="💾 Инициализация резервного копирования базы данных..." + ) + await backup_database() + await callback_query.message.answer( + text="✅ Резервная копия успешно создана и отправлена администратору." + ) From 0b0fca8648ed9014413c6f8158b945c4842d916e Mon Sep 17 00:00:00 2001 From: hteppl Date: Sat, 11 Jan 2025 02:17:02 +0300 Subject: [PATCH 05/23] Add more base functions for creating singleton admin keyboards --- keyboards/admin/panel_kb.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/keyboards/admin/panel_kb.py b/keyboards/admin/panel_kb.py index b8e9133c..93971469 100644 --- a/keyboards/admin/panel_kb.py +++ b/keyboards/admin/panel_kb.py @@ -60,9 +60,8 @@ def build_management_kb() -> InlineKeyboardMarkup: text="🔄 Перезагрузить бота", callback_data=AdminPanelCallback(action="restart").pack() ) - builder.button( - text="⬅️ Назад", - callback_data="admin" + builder.row( + build_admin_back_btn("admin") ) return builder.as_markup() @@ -73,9 +72,8 @@ def build_restart_kb() -> InlineKeyboardMarkup: text="✅ Да, перезапустить", callback_data=AdminPanelCallback(action="restart_confirm").pack() ) - builder.button( - text="🔙 Назад", - callback_data="admin" + builder.row( + build_admin_back_btn("admin") ) return builder.as_markup() @@ -86,5 +84,15 @@ def build_admin_back_kb(action: str) -> InlineKeyboardMarkup: def build_admin_singleton_kb(text: str, action: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button(text=text, callback_data=AdminPanelCallback(action=action).pack()) + builder.row( + build_admin_btn(text, action) + ) return builder.as_markup() + + +def build_admin_back_btn(action: str) -> InlineKeyboardMarkup: + return build_admin_btn("🔙 Назад", action) + + +def build_admin_btn(text: str, action: str) -> InlineKeyboardButton: + return InlineKeyboardButton(text=text, callback_data=AdminPanelCallback(action=action).pack()) From c9664c136b99f1938236e572b709bf17052ebf42 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sat, 11 Jan 2025 02:17:54 +0300 Subject: [PATCH 06/23] Implement admin restart handler + also handle AdminPanelCallback for back buttons --- handlers/admin/admin_panel.py | 8 ++++++ handlers/admin/admin_restart.py | 50 +++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 handlers/admin/admin_restart.py diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index 3df31a34..d1ffbdf7 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -9,6 +9,14 @@ from keyboards.admin.panel_kb import build_panel_kb, AdminPanelCallback, build_m router = Router() +@router.callback_query( + AdminPanelCallback.filter(F.action == "admin"), + IsAdminFilter() +) +async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext): + await handle_admin_message(callback_query.message, state) + + @router.callback_query( F.data == "admin", IsAdminFilter() diff --git a/handlers/admin/admin_restart.py b/handlers/admin/admin_restart.py new file mode 100644 index 00000000..47cbc134 --- /dev/null +++ b/handlers/admin/admin_restart.py @@ -0,0 +1,50 @@ +import subprocess + +from aiogram import F, Router +from aiogram.types import CallbackQuery + +from filters.admin import IsAdminFilter +from keyboards.admin.panel_kb import build_restart_kb, AdminPanelCallback, build_admin_back_kb + +router = Router() + + +@router.callback_query( + AdminPanelCallback.filter(F.action == "restart"), + IsAdminFilter(), +) +async def handle_restart(callback_query: CallbackQuery): + kb = build_restart_kb() + await callback_query.message.answer( + text="🤔 Вы уверены, что хотите перезапустить бота?", + reply_markup=kb, + ) + + +@router.callback_query( + AdminPanelCallback.filter(F.action == "restart_confirm"), + IsAdminFilter(), +) +async def confirm_restart_bot(callback_query: CallbackQuery): + kb = build_admin_back_kb("admin") + try: + subprocess.run( + ["sudo", "systemctl", "restart", "bot.service"], + check=True, + capture_output=True, + text=True, + ) + await callback_query.message.answer( + text="🔄 Бот успешно перезапущен.", + reply_markup=kb + ) + except subprocess.CalledProcessError: + await callback_query.message.answer( + text="🔄 Бот успешно перезапущен.", + reply_markup=kb + ) + except Exception as e: + await callback_query.message.answer( + text=f"⚠️ Ошибка при перезагрузке бота: {e.stderr}", + reply_markup=kb + ) From 97d598a2938bc10331ea3a3dec43bd1117076726 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sat, 11 Jan 2025 02:24:16 +0300 Subject: [PATCH 07/23] Implement admin stats handler --- handlers/admin/admin_stats.py | 178 ++++++++++++++++++++++++++++++++++ keyboards/admin/stats_kb.py | 24 +++++ 2 files changed, 202 insertions(+) create mode 100644 handlers/admin/admin_stats.py create mode 100644 keyboards/admin/stats_kb.py diff --git a/handlers/admin/admin_stats.py b/handlers/admin/admin_stats.py new file mode 100644 index 00000000..113a6caa --- /dev/null +++ b/handlers/admin/admin_stats.py @@ -0,0 +1,178 @@ +from datetime import datetime +from io import BytesIO +from typing import Any + +from aiogram import F, Router +from aiogram.types import CallbackQuery, BufferedInputFile + +from filters.admin import IsAdminFilter +from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb +from keyboards.admin.stats_kb import build_stats_kb +from logger import logger + +router = Router() + + +@router.callback_query( + AdminPanelCallback.filter(F.action == "stats"), + IsAdminFilter(), +) +async def user_stats_menu(callback_query: CallbackQuery, session: Any): + try: + total_users = await session.fetchval("SELECT COUNT(*) FROM connections") + total_keys = await session.fetchval("SELECT COUNT(*) FROM keys") + total_referrals = await session.fetchval("SELECT COUNT(*) FROM referrals") + + total_payments_today = await session.fetchval( + "SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= CURRENT_DATE" + ) + total_payments_week = await session.fetchval( + "SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('week', CURRENT_DATE)" + ) + total_payments_month = await session.fetchval( + "SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('month', CURRENT_DATE)" + ) + total_payments_all_time = await session.fetchval( + "SELECT COALESCE(SUM(amount), 0) FROM payments" + ) + + active_keys = await session.fetchval( + "SELECT COUNT(*) FROM keys WHERE expiry_time > $1", + int(datetime.utcnow().timestamp() * 1000), + ) + expired_keys = total_keys - active_keys + + stats_message = ( + f"📊 Подробная статистика проекта:\n\n" + f"👥 Пользователи:\n" + f" 🌐 Зарегистрировано: {total_users}\n" + f" 🤝 Привлеченных рефералов: {total_referrals}\n\n" + f"🔑 Ключи:\n" + f" 🌈 Всего сгенерировано: {total_keys}\n" + f" ✅ Действующих: {active_keys}\n" + f" ❌ Просроченных: {expired_keys}\n\n" + f"💰 Финансовая статистика:\n" + f" 📅 За день: {total_payments_today} ₽\n" + f" 📆 За неделю: {total_payments_week} ₽\n" + f" 📆 За месяц: {total_payments_month} ₽\n" + f" 🏦 За все время: {total_payments_all_time} ₽\n" + ) + + kb = build_stats_kb() + + await callback_query.message.edit_text( + text=stats_message, + reply_markup=kb + ) + except Exception as e: + logger.error(f"Error in user_stats_menu: {e}") + + +@router.callback_query( + AdminPanelCallback.filter(F.action == "stats_export_users_csv"), + IsAdminFilter(), +) +async def export_users_csv(callback_query: CallbackQuery, session: Any): + kb = build_admin_back_kb("stats") + + try: + users = await session.fetch( + """ + SELECT + u.tg_id, + u.username, + u.first_name, + u.last_name, + u.language_code, + u.is_bot, + c.balance, + c.trial + FROM users u + LEFT JOIN connections c ON u.tg_id = c.tg_id + """ + ) + + if not users: + await callback_query.message.answer( + text="📭 Нет пользователей для экспорта.", + reply_markup=kb + ) + return + + csv_data = "tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n" # Заголовки CSV + for user in users: + csv_data += f"{user['tg_id']},{user['username']},{user['first_name']},{user['last_name']},{user['language_code']},{user['is_bot']},{user['balance']},{user['trial']}\n" + + file_name = BytesIO(csv_data.encode("utf-8-sig")) + file_name.seek(0) + + file = BufferedInputFile(file_name.getvalue(), filename="users_export.csv") + + await callback_query.message.answer_document( + file, + caption="📥 Экспорт пользователей в CSV", + reply_markup=kb + ) + file_name.close() + + except Exception as e: + logger.error(f"Ошибка при экспорте пользователей в CSV: {e}") + await callback_query.message.answer( + text="❗ Произошла ошибка при экспорте пользователей.", + reply_markup=kb + ) + + +@router.callback_query( + AdminPanelCallback.filter(F.action == "stats_export_payments_csv"), + IsAdminFilter(), +) +async def export_payments_csv(callback_query: CallbackQuery, session: Any): + kb = build_admin_back_kb("stats") + + try: + payments = await session.fetch( + """ + SELECT + u.tg_id, + u.username, + u.first_name, + u.last_name, + p.amount, + p.payment_system, + p.status, + p.created_at + FROM users u + JOIN payments p ON u.tg_id = p.tg_id + """ + ) + + if not payments: + await callback_query.message.answer( + text="📭 Нет платежей для экспорта.", + reply_markup=kb + ) + return + + csv_data = "tg_id,username,first_name,last_name,amount,payment_system,status,created_at\n" # Заголовки CSV + for payment in payments: + csv_data += f"{payment['tg_id']},{payment['username']},{payment['first_name']},{payment['last_name']},{payment['amount']},{payment['payment_system']},{payment['status']},{payment['created_at']}\n" + + file_name = BytesIO(csv_data.encode("utf-8-sig")) + file_name.seek(0) + + file = BufferedInputFile(file_name.getvalue(), filename="payments_export.csv") + + await callback_query.message.answer_document( + document=file, + caption="📥 Экспорт платежей в CSV", + reply_markup=kb + ) + file_name.close() + + except Exception as e: + logger.error(f"Ошибка при экспорте платежей в CSV: {e}") + await callback_query.message.answer( + text="❗ Произошла ошибка при экспорте платежей.", + reply_markup=kb + ) diff --git a/keyboards/admin/stats_kb.py b/keyboards/admin/stats_kb.py new file mode 100644 index 00000000..3e3b9b04 --- /dev/null +++ b/keyboards/admin/stats_kb.py @@ -0,0 +1,24 @@ +from aiogram.types import InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from keyboards.admin.panel_kb import build_admin_back_btn, AdminPanelCallback + + +def build_stats_kb() -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button( + text="🔄 Обновить", + callback_data=AdminPanelCallback(action="stats").pack() + ) + builder.button( + text="📥 Выгрузить пользователей в CSV", + callback_data=AdminPanelCallback(action="stats_export_users_csv").pack() + ) + builder.button( + text="📥 Выгрузить оплаты в CSV", + callback_data=AdminPanelCallback(action="stats_export_payments_csv").pack() + ) + builder.row( + build_admin_back_btn("admin") + ) + return builder.as_markup() From e957fbdfa18d23101323bebc635735d81d939440 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 12 Jan 2025 16:58:17 +0300 Subject: [PATCH 08/23] Implement admin users editor handler --- handlers/admin/__init__.py | 2 +- handlers/admin/admin_users_editor.py | 596 +++++++++++++++++++++++++++ keyboards/admin/users_editor_kb.py | 116 ++++++ 3 files changed, 713 insertions(+), 1 deletion(-) create mode 100644 handlers/admin/admin_users_editor.py create mode 100644 keyboards/admin/users_editor_kb.py diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py index 39109027..5f676eed 100644 --- a/handlers/admin/__init__.py +++ b/handlers/admin/__init__.py @@ -5,7 +5,7 @@ from aiogram import Router from .admin_coupons import router as coupons_router from .admin_panel import router as panel_router from .admin_servers import router as servers_router -from .admin_user_editor import router as user_editor_router +from .admin_users_editor import router as user_editor_router router = Router(name="admins_main_router") diff --git a/handlers/admin/admin_users_editor.py b/handlers/admin/admin_users_editor.py new file mode 100644 index 00000000..182c3599 --- /dev/null +++ b/handlers/admin/admin_users_editor.py @@ -0,0 +1,596 @@ +import asyncio +from datetime import datetime +from typing import Any + +from aiogram import Bot, F, Router, types +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from aiogram.types import CallbackQuery +from config import TOTAL_GB + +from database import delete_user_data, get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry +from filters.admin import IsAdminFilter +from handlers.keys.key_utils import ( + delete_key_from_cluster, + delete_key_from_db, + renew_key_in_cluster, +) +from handlers.utils import sanitize_key_name +from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb +from keyboards.admin.users_editor_kb import build_user_edit_kb, build_key_edit_kb, build_key_delete_kb, \ + build_user_delete_kb, AdminUserEditorCallback +from logger import logger + +router = Router() + + +class UserEditorState(StatesGroup): + # search + waiting_for_user_data = State() + waiting_for_key_name = State() + # updating data + waiting_for_new_balance = State() + waiting_for_expiry_time = State() + waiting_for_message_text = State() + + +@router.callback_query( + AdminPanelCallback.filter(F.action == "users_search"), + IsAdminFilter(), +) +async def handle_users_search( + callback_query: CallbackQuery, + state: FSMContext +): + text = ( + "🔍 Введите ID или Username пользователя для поиска:" + "\n— ID - числовой айди пользователя" + "\n— Username - юзернейм пользователя, начинающийся с @ или https://t.me/" + ) + + await state.set_state(UserEditorState.waiting_for_user_data) + await callback_query.message.answer( + text=text, + reply_markup=build_admin_back_kb("users_editor") + ) + + +@router.callback_query( + AdminPanelCallback.filter(F.action == "users_search_key"), + IsAdminFilter(), +) +async def handle_users_search_key( + callback_query: CallbackQuery, + state: FSMContext +): + await state.set_state(UserEditorState.waiting_for_key_name) + await callback_query.message.answer( + text="🔑 Введите имя ключа для поиска:", + reply_markup=build_admin_back_kb("users_editor") + ) + + +@router.message( + UserEditorState.waiting_for_user_data, + IsAdminFilter() +) +async def handle_user_data_input( + message: types.Message, + state: FSMContext, + session: Any +): + kb = build_admin_back_kb("users_editor") + + if not message.text: + await message.reply( + text="💢 Пожалуйста, отправьте текстовое сообщение.", + reply_markup=kb + ) + return + + if message.text.isdigit(): + tg_id = int(message.text) + else: + # Удаление '@' символа в начале сообщения + username = message.text.strip().lstrip('@') + # Удаление начала ссылки на профиль + username = username.replace('https://t.me/', '') + + user = await session.fetchrow( + "SELECT tg_id FROM users WHERE username = $1", username + ) + + if not user: + await message.answer( + text="🔍 Пользователь с указанным username не найден. 🚫", + reply_markup=kb, + ) + await state.clear() + return + + tg_id = user["tg_id"] + + await process_user_search(message, state, session, tg_id) + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_send_message"), + IsAdminFilter(), +) +async def handle_send_message( + callback_query: types.CallbackQuery, + callback_data: AdminUserEditorCallback, + state: FSMContext +): + tg_id = callback_data.data + await callback_query.message.answer( + "✉️ Введите текст сообщения, которое вы хотите отправить пользователю." + ) + await state.update_data(target_tg_id=tg_id) + await state.set_state(UserEditorState.waiting_for_message_text) + + +@router.message( + UserEditorState.waiting_for_message_text, + IsAdminFilter() +) +async def handle_message_text_input( + message: types.Message, + state: FSMContext, + bot: Bot +): + data = await state.get_data() + target_tg_id = data.get("target_tg_id") + + if not target_tg_id: + await message.answer("🚫 Ошибка: ID пользователя не найден.") + await state.clear() + return + + try: + await bot.send_message(chat_id=target_tg_id, text=message.text) + await message.answer("✅ Сообщение успешно отправлено.") + except Exception as e: + await message.answer(f"❌ Не удалось отправить сообщение: {e}") + + await state.clear() + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_trial_restore"), + IsAdminFilter(), +) +async def handle_restore_trial( + callback_query: types.CallbackQuery, + callback_data: AdminUserEditorCallback, + session: Any +): + tg_id = int(callback_data.data) + await restore_trial(tg_id, session) + await callback_query.message.answer( + text="✅ Триал успешно восстановлен.", + reply_markup=build_admin_back_kb("admin") + ) + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_balance_change"), + IsAdminFilter() +) +async def process_balance_change( + callback_query: CallbackQuery, + callback_data: AdminUserEditorCallback, + state: FSMContext +): + tg_id = int(callback_data.data) + await state.update_data(tg_id=tg_id) + await callback_query.message.answer( + text="💸 Введите новую сумму баланса:", + reply_markup=build_admin_back_kb("users_editor") + ) + await state.set_state(UserEditorState.waiting_for_new_balance) + + +@router.message( + UserEditorState.waiting_for_new_balance, + IsAdminFilter() +) +async def handle_new_balance_input( + message: types.Message, + state: FSMContext, + session: Any +): + if not message.text.isdigit() or int(message.text) < 0: + await message.answer( + text="❌ Пожалуйста, введите корректную сумму для изменения баланса.", + reply_markup=build_admin_back_kb("users_editor"), + ) + return + + new_balance = int(message.text) + user_data = await state.get_data() + tg_id = user_data.get("tg_id") + + await session.execute( + "UPDATE connections SET balance = $1 WHERE tg_id = $2", + new_balance, + tg_id, + ) + + await message.answer( + text=f"✅ Баланс успешно изменен на {new_balance}.", + reply_markup=build_admin_back_kb("admin") + ) + await state.clear() + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_key_edit"), + IsAdminFilter() +) +async def process_key_edit( + callback_query: CallbackQuery, + callback_data: AdminUserEditorCallback, + session: Any +): + email = callback_data.data + key_details = await get_key_details(email, session) + + if not key_details: + await callback_query.message.answer( + text="🔍 Информация о ключе не найдена. 🚫", + reply_markup=build_admin_back_kb("users_editor"), + ) + return + + response_message = ( + f"🔑 Ключ: {key_details['key']}\n" + f"⏰ Дата истечения: {key_details['expiry_date']}\n" + f"💰 Баланс пользователя: {key_details['balance']}\n" + f"🌐 Кластер: {key_details['server_name']}" + ) + + await callback_query.message.answer( + text=response_message, + reply_markup=build_key_edit_kb(key_details, email) + ) + + +@router.message( + UserEditorState.waiting_for_key_name, + IsAdminFilter() +) +async def handle_key_name_input( + message: types.Message, + state: FSMContext, + session: Any +): + key_name = sanitize_key_name(message.text) + key_details = await get_key_details(key_name, session) + + if not key_details: + await message.answer( + text="🚫 Пользователь с указанным именем ключа не найден.", + reply_markup=build_admin_back_kb("users_editor") + ) + await state.clear() + return + + response_message = ( + f"🔑 Ключ: {key_details['key']}\n" + f"⏰ Дата истечения: {key_details['expiry_date']}\n" + f"💰 Баланс пользователя: {key_details['balance']}\n" + f"🌐 Сервер: {key_details['server_name']}" + ) + + await message.answer( + text=response_message, + reply_markup=build_key_edit_kb(key_details, key_name) + ) + await state.clear() + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_change_expiry"), + IsAdminFilter() +) +async def prompt_expiry_change( + callback_query: CallbackQuery, + callback_data: AdminUserEditorCallback, + state: FSMContext +): + email = callback_data.data + await callback_query.message.answer( + text=f"⏳ Введите новое время истечения для ключа {email} в формате YYYY-MM-DD HH:MM:SS:" + ) + await state.update_data(email=email) + await state.set_state(UserEditorState.waiting_for_expiry_time) + + +@router.message( + UserEditorState.waiting_for_expiry_time, + IsAdminFilter() +) +async def handle_expiry_time_input( + message: types.Message, + state: FSMContext, + session: Any +): + user_data = await state.get_data() + email = user_data.get("email") + + if not email: + await message.answer( + text="📧 Email не найден в состоянии. 🚫", + reply_markup=build_admin_back_kb("users_editor") + ) + await state.clear() + return + + try: + expiry_time_str = message.text + expiry_time = int( + datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").timestamp() * 1000 + ) + + client_id = await get_client_id_by_email(email) + if client_id is None: + await message.answer( + text=f"🚫 Клиент с email {email} не найден. 🔍", + reply_markup=build_admin_back_kb("users_editor"), + ) + await state.clear() + return + + record = await session.fetchrow( + "SELECT server_id FROM keys WHERE client_id = $1", client_id + ) + if not record: + await message.answer( + text="🚫 Клиент не найден в базе данных. 🔍", + reply_markup=build_admin_back_kb("users_editor"), + ) + await state.clear() + return + + clusters = await get_servers_from_db() + + async def update_key_on_all_servers(): + tasks = [ + asyncio.create_task( + renew_key_in_cluster( + cluster_name, + email, + client_id, + expiry_time, + total_gb=TOTAL_GB, + ) + ) + for cluster_name in clusters + ] + + await asyncio.gather(*tasks) + + await update_key_on_all_servers() + + await update_key_expiry(client_id, expiry_time) + + response_message = f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах." + + await message.answer( + text=response_message, + reply_markup=build_admin_back_kb("admin") + ) + except ValueError: + await message.answer( + text="❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.", + reply_markup=build_admin_back_kb("users_editor"), + ) + except Exception as e: + logger.error(e) + await state.clear() + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_delete_key"), + IsAdminFilter() +) +async def process_callback_delete_key( + callback_query: types.CallbackQuery, + callback_data: AdminUserEditorCallback, + session: Any +): + email = callback_data.data + client_id = await session.fetchval( + "SELECT client_id FROM keys WHERE email = $1", email + ) + + if client_id is None: + await callback_query.message.answer( + text="🔍 Ключ не найден. 🚫", + reply_markup=build_admin_back_kb("users_editor") + ) + return + + await callback_query.message.answer( + text="❓ Вы уверены, что хотите удалить ключ?", + reply_markup=build_key_delete_kb(client_id), + ) + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_delete_key_confirm"), + IsAdminFilter() +) +async def process_callback_confirm_delete( + callback_query: types.CallbackQuery, + callback_data: AdminUserEditorCallback, + session: Any +): + client_id = callback_data.data + record = await session.fetchrow( + "SELECT email FROM keys WHERE client_id = $1", client_id + ) + + kb = build_admin_back_kb("users_editor") + + if record: + clusters = await get_servers_from_db() + + async def delete_key_from_servers(email, client_id): + tasks = [] + for cluster_name, cluster_servers in clusters.items(): + for server in cluster_servers: + tasks.append( + delete_key_from_cluster(cluster_name, email, client_id) + ) + await asyncio.gather(*tasks) + + await delete_key_from_servers(record["email"], client_id) + await delete_key_from_db(client_id, session) + + await callback_query.message.answer( + text="✅ Ключ успешно удален.", + reply_markup=kb + ) + else: + await callback_query.message.answer( + text="🚫 Ключ не найден или уже удален.", + reply_markup=kb + ) + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_delete_user"), + IsAdminFilter() +) +async def delete_user( + callback_query: types.CallbackQuery, + callback_data: AdminUserEditorCallback, + session: Any +): + tg_id = int(callback_data.data) + key_records = await session.fetch("SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id) + + async def delete_keys_from_servers(): + try: + tasks = [] + for email, client_id in key_records: + servers = await get_servers_from_db() + for cluster_id, cluster in servers.items(): + tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) + await asyncio.gather(*tasks) + except Exception as e: + logger.error(f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}") + + await delete_keys_from_servers() + + try: + await delete_user_data(session, tg_id) + await callback_query.message.answer( + text=f"🗑️ Пользователь с ID {tg_id} был удален.", + reply_markup=build_admin_back_kb("users_editor") + ) + except Exception as e: + logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}") + await callback_query.message.answer( + text=f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова." + ) + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_delete_user_confirm"), + IsAdminFilter() +) +async def confirm_delete_user( + callback_query: types.CallbackQuery, + callback_data: AdminUserEditorCallback +): + tg_id = int(callback_data.data) + await callback_query.message.answer( + text=f"Вы уверены, что хотите удалить пользователя с ID {tg_id}?", + reply_markup=build_user_delete_kb(tg_id) + ) + + +async def process_user_search(message: types.Message, state: FSMContext, session: Any, tg_id: int) -> None: + username = await session.fetchval( + "SELECT username FROM users WHERE tg_id = $1", tg_id + ) + balance = await session.fetchval( + "SELECT balance FROM connections WHERE tg_id = $1", tg_id + ) + + if balance is None: + await message.answer( + text="🚫 Пользователь с указанным ID не найден!", + reply_markup=build_admin_back_kb("users_editor"), + ) + return + + key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) + referral_count = await session.fetchval( + "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id + ) + + text = ( + f"📊 Информация о пользователе:\n\n" + f"🆔 ID пользователя: {tg_id}\n" + f"👤 Логин пользователя: @{username}\n" + f"💰 Баланс: {balance}\n" + f"👥 Количество рефералов: {referral_count}\n" + f"🔑 Ключи (для редактирования нажмите на ключ):" + ) + + await message.answer( + text=text, + reply_markup=build_user_edit_kb(tg_id, key_records) + ) + + await state.clear() + + +async def get_key_details(email, session): + record = await session.fetchrow( + """ + SELECT k.key, k.expiry_time, k.server_id, c.tg_id, c.balance + FROM keys k + JOIN connections c ON k.tg_id = c.tg_id + WHERE k.email = $1 + """, + email, + ) + + if not record: + return None + + servers = await get_servers_from_db() + + cluster_name = "Неизвестный кластер" + for cluster_name, cluster_servers in servers.items(): + if any( + server["inbound_id"] == record["server_id"] for server in cluster_servers + ): + cluster_name = cluster_name + break + + expiry_date = datetime.utcfromtimestamp(record["expiry_time"] / 1000) + current_date = datetime.utcnow() + time_left = expiry_date - current_date + + if time_left.total_seconds() <= 0: + days_left_message = "Ключ истек." + elif time_left.days > 0: + days_left_message = f"Осталось дней: {time_left.days}" + else: + hours_left = time_left.seconds // 3600 + days_left_message = f"Осталось часов: {hours_left}" + + return { + "key": record["key"], + "expiry_date": expiry_date.strftime("%d %B %Y года"), + "days_left_message": days_left_message, + "server_name": cluster_name, + "balance": record["balance"], + "tg_id": record["tg_id"], + } diff --git a/keyboards/admin/users_editor_kb.py b/keyboards/admin/users_editor_kb.py new file mode 100644 index 00000000..f93e2dcd --- /dev/null +++ b/keyboards/admin/users_editor_kb.py @@ -0,0 +1,116 @@ +from aiogram.filters.callback_data import CallbackData +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from keyboards.admin.panel_kb import build_admin_back_btn + + +class AdminUserEditorCallback(CallbackData, prefix='admin_users_editor'): + action: str + data: str + + +def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + for email in key_records: + builder.button( + text=f"🔑 {email}", + callback_data=AdminUserEditorCallback( + action="users_key_edit", + data=email + ).pack() + ) + + builder.button( + text="📝 Изменить баланс", + callback_data=AdminUserEditorCallback( + action="users_balance_change", + data=tg_id + ).pack() + ) + builder.button( + text="🔄 Восстановить пробник", + callback_data=AdminUserEditorCallback( + action="users_trial_restore", + data=tg_id + ).pack() + ) + builder.button( + text="❌ Удалить клиента", + callback_data=AdminUserEditorCallback( + action="users_delete_user", + data=tg_id + ).pack() + ) + builder.row( + build_admin_back_btn("users_editor") + ) + return builder.as_markup() + + +def build_user_delete_kb(tg_id: int): + builder = InlineKeyboardBuilder() + builder.button( + text="❌ Удалить клиента", + callback_data=AdminUserEditorCallback( + action="users_delete_user_confirm", + data=tg_id + ).pack() + ) + builder.row( + build_admin_back_btn("users_editor") + ) + return builder.as_markup() + + +def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="ℹ️ Получить информацию о юзере", + callback_data=AdminUserEditorCallback( + action="users_info", + data=key_details["tg_id"] + ).pack() + ) + ) + builder.row( + InlineKeyboardButton( + text="⏳ Изменить время истечения", + callback_data=AdminUserEditorCallback( + action="users_change_expiry", + data=email + ).pack() + ) + ) + builder.row( + InlineKeyboardButton( + text="❌ Удалить ключ", + callback_data=AdminUserEditorCallback( + action="users_delete_key", + data=email + ).pack() + ) + ) + builder.row( + build_admin_back_btn("users_editor") + ) + return builder.as_markup() + + +def build_key_delete_kb(client_id: str) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="✅ Да, удалить", + callback_data=AdminUserEditorCallback( + action="users_delete_key_confirm", + data=client_id + ).pack() + ) + ) + builder.row( + build_admin_back_btn("users_editor") + ) + return builder.as_markup() From 9b98faedfd76fb02b6d66e3fbf3e1d38f1aa74e9 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 12 Jan 2025 17:01:39 +0300 Subject: [PATCH 09/23] Rename admin users editor handler to users handler --- handlers/admin/__init__.py | 4 ++-- handlers/admin/{admin_users_editor.py => admin_users.py} | 2 +- keyboards/admin/{users_editor_kb.py => users_kb.py} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename handlers/admin/{admin_users_editor.py => admin_users.py} (99%) rename keyboards/admin/{users_editor_kb.py => users_kb.py} (100%) diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py index 5f676eed..549705b6 100644 --- a/handlers/admin/__init__.py +++ b/handlers/admin/__init__.py @@ -5,7 +5,7 @@ from aiogram import Router from .admin_coupons import router as coupons_router from .admin_panel import router as panel_router from .admin_servers import router as servers_router -from .admin_users_editor import router as user_editor_router +from .admin_users import router as users_router router = Router(name="admins_main_router") @@ -13,5 +13,5 @@ router.include_routers( panel_router, servers_router, coupons_router, - user_editor_router, + users_router, ) diff --git a/handlers/admin/admin_users_editor.py b/handlers/admin/admin_users.py similarity index 99% rename from handlers/admin/admin_users_editor.py rename to handlers/admin/admin_users.py index 182c3599..be8b5361 100644 --- a/handlers/admin/admin_users_editor.py +++ b/handlers/admin/admin_users.py @@ -17,7 +17,7 @@ from handlers.keys.key_utils import ( ) from handlers.utils import sanitize_key_name from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb -from keyboards.admin.users_editor_kb import build_user_edit_kb, build_key_edit_kb, build_key_delete_kb, \ +from keyboards.admin.users_kb import build_user_edit_kb, build_key_edit_kb, build_key_delete_kb, \ build_user_delete_kb, AdminUserEditorCallback from logger import logger diff --git a/keyboards/admin/users_editor_kb.py b/keyboards/admin/users_kb.py similarity index 100% rename from keyboards/admin/users_editor_kb.py rename to keyboards/admin/users_kb.py From ab27328cb7e9cd94e9cc37a7c668ea8c1ee29903 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 12 Jan 2025 17:07:57 +0300 Subject: [PATCH 10/23] Implement admin servers handler --- handlers/admin/admin_servers.py | 599 +++++++++++++------------------- keyboards/admin/servers_kb.py | 120 +++++++ 2 files changed, 357 insertions(+), 362 deletions(-) create mode 100644 keyboards/admin/servers_kb.py diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index a2ee4ed4..0cab2ec4 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -1,24 +1,22 @@ -import asyncio - import asyncpg from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import InlineKeyboardButton -from aiogram.utils.keyboard import InlineKeyboardBuilder -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, get_servers_from_db from filters.admin import IsAdminFilter -from handlers.keys.key_utils import create_key_on_cluster -from logger import logger +from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb +from keyboards.admin.servers_kb import build_cancel_kb, build_manage_server_kb, \ + build_delete_server_kb, \ + build_manage_cluster_kb, build_clusters_editor_kb, AdminServerEditorCallback router = Router() -class UserEditorState(StatesGroup): +class AdminServersEditor(StatesGroup): waiting_for_cluster_name = State() waiting_for_api_url = State() waiting_for_inbound_id = State() @@ -26,112 +24,103 @@ class UserEditorState(StatesGroup): waiting_for_subscription_url = State() -@router.callback_query(F.data == "servers_editor", IsAdminFilter()) -async def handle_servers_editor(callback_query: types.CallbackQuery): +@router.callback_query( + AdminPanelCallback.filter(F.action == "servers_editor"), + IsAdminFilter(), +) +async def handle_servers_editor( + callback_query: types.CallbackQuery +): servers = await get_servers_from_db() - builder = InlineKeyboardBuilder() - - for cluster_name, cluster_servers in servers.items(): - builder.row( - InlineKeyboardButton( - text=f"⚙️ {cluster_name}", callback_data=f"manage_cluster|{cluster_name}" - ) - ) - - builder.row( - InlineKeyboardButton(text="➕ Добавить кластер", callback_data="add_cluster") - ) - builder.row(InlineKeyboardButton(text="🔙 Назад в админку", callback_data="admin")) - - await callback_query.message.answer( + text = ( "🔧 Управление кластерами\n\n" "📌 Здесь вы можете добавить новый кластер.\n\n" "🌐 Кластеры — это пространство серверов, в пределах которого создается подписка.\n" "💡 Если вы хотите выдавать по 1 серверу, то добавьте всего 1 сервер в кластер.\n\n" - "⚠️ Важно: Кластеры удаляются автоматически, если удалить все серверы внутри них.\n\n", - parse_mode="HTML", - reply_markup=builder.as_markup(), + "⚠️ Важно: Кластеры удаляются автоматически, если удалить все серверы внутри них.\n\n" ) - -@router.callback_query(F.data == "add_cluster", IsAdminFilter()) -async def handle_add_cluster(callback_query: types.CallbackQuery, state: FSMContext): await callback_query.message.answer( + text=text, + parse_mode="HTML", + reply_markup=build_clusters_editor_kb(servers), + ) + + +@router.callback_query( + AdminPanelCallback.filter(F.action == "clusters_add"), + IsAdminFilter(), +) +async def handle_add_cluster( + callback_query: types.CallbackQuery, + state: FSMContext +): + text = ( "🔧 Введите имя нового кластера:\n\n" - "Имя кластера должно быть уникальным и на английском языке.\n" - "Пример: cluster2 или us_east_1", + "Имя кластера должно быть уникальным!\n" + "Пример: cluster1 или us_east_1" + ) + + await callback_query.message.answer( + text=text, parse_mode="HTML", ) - await state.set_state(UserEditorState.waiting_for_cluster_name) + await state.set_state(AdminServersEditor.waiting_for_cluster_name) -@router.message(UserEditorState.waiting_for_cluster_name, IsAdminFilter()) -async def handle_cluster_name_input(message: types.Message, state: FSMContext): - cluster_name = message.text.strip() - - if cluster_name == "❌ Отменить": - await state.clear() - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔧 Управление кластерами", callback_data="servers_editor" - ) - ) +@router.message( + AdminServersEditor.waiting_for_cluster_name, + IsAdminFilter() +) +async def handle_cluster_name_input( + message: types.Message, + state: FSMContext +): + if not message.text: await message.answer( - "Процесс создания кластера отменен. Вы вернулись в меню управления серверами.", - reply_markup=builder.as_markup(), + text="❌ Имя кластера не может быть пустым. Попробуйте снова." ) return - if not cluster_name: - await message.answer("❌ Имя кластера не может быть пустым. Попробуйте снова.") - return - + cluster_name = message.text.strip() await state.update_data(cluster_name=cluster_name) - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") + text = ( + f"Введите имя сервера для кластера {cluster_name}:\n\n" + "Рекомендуется указать локацию сервера в имени.\n\n" + "Пример: server-frankfurt1, fra1" ) await message.answer( - f"Введите имя сервера для кластера {cluster_name}:\n\n" - "Рекомендуется указать локацию сервера в имени.\n\n" - "Пример: server-asia, server-europe", + text=text, parse_mode="HTML", - reply_markup=builder.as_markup(), + reply_markup=build_admin_back_kb("servers_editor"), ) - await state.set_state(UserEditorState.waiting_for_server_name) + + await state.set_state(AdminServersEditor.waiting_for_server_name) -@router.message(UserEditorState.waiting_for_server_name, IsAdminFilter()) -async def handle_server_name_input(message: types.Message, state: FSMContext): +@router.message( + AdminServersEditor.waiting_for_server_name, + IsAdminFilter() +) +async def handle_server_name_input( + message: types.Message, + state: FSMContext +): + if not message.text: + await message.answer( + text="❌ Имя сервера не может быть пустым. Попробуйте снова." + ) + return + server_name = message.text.strip() - if server_name == "❌ Отменить": - await state.clear() - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔧 Управление кластерами", callback_data="servers_editor" - ) - ) + if not await check_unique_server_name(server_name): await message.answer( - "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", - reply_markup=builder.as_markup(), - ) - return - - if not server_name: - await message.answer("❌ Имя сервера не может быть пустым. Попробуйте снова.") - return - - server_unique = await check_unique_server_name(server_name) - if not server_unique: - await message.answer( - "❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя." + text="❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя." ) return @@ -139,124 +128,109 @@ async def handle_server_name_input(message: types.Message, state: FSMContext): cluster_name = user_data.get("cluster_name") await state.update_data(server_name=server_name) - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") - ) - - await message.answer( + text = ( f"Введите API URL для сервера {server_name} в кластере {cluster_name}:\n\n" "API URL должен быть в следующем формате:\n\n" "https://your_domain:port/panel_path\n\n" - "URL должен быть без слэша на конце!\n", - parse_mode="HTML", - reply_markup=builder.as_markup(), + "URL должен быть без слэша на конце!\n" ) - await state.set_state(UserEditorState.waiting_for_api_url) + + await message.answer( + text=text, + parse_mode="HTML", + reply_markup=build_admin_back_kb("servers_editor"), + ) + + await state.set_state(AdminServersEditor.waiting_for_api_url) -@router.message(UserEditorState.waiting_for_api_url, IsAdminFilter()) -async def handle_api_url_input(message: types.Message, state: FSMContext): - api_url = message.text.strip() - - if api_url == "❌ Отменить": - await state.clear() - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔧 Управление кластерами", callback_data="servers_editor" - ) - ) +@router.message( + AdminServersEditor.waiting_for_api_url, + IsAdminFilter() +) +async def handle_api_url_input( + message: types.Message, + state: FSMContext +): + if not message.text or not message.text.strip().startswith("https://"): await message.answer( - "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", - reply_markup=builder.as_markup(), - ) - return - - if not api_url.startswith("https://"): - await message.answer( - "❌ API URL должен начинаться с https://. Попробуйте снова.", + text="❌ API URL должен начинаться с https://. Попробуйте снова.", parse_mode="HTML", ) return - api_url = api_url.rstrip("/") + api_url = message.text.strip().rstrip("/") user_data = await state.get_data() cluster_name = user_data.get("cluster_name") server_name = user_data.get("server_name") await state.update_data(api_url=api_url) - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") - ) - - await message.answer( + text = ( f"Введите subscription_url для сервера {server_name} в кластере {cluster_name}:\n\n" "Subscription URL должен быть в следующем формате:\n\n" "https://your_domain:port_sub/sub_path\n\n" "URL должен быть без слэша и имени клиента на конце!\n" - "Его можно увидеть в панели 3x-ui в информации о клиенте.", - parse_mode="HTML", - reply_markup=builder.as_markup(), + "Его можно увидеть в панели 3x-ui в информации о клиенте." ) - await state.set_state(UserEditorState.waiting_for_subscription_url) + + await message.answer( + text=text, + parse_mode="HTML", + reply_markup=build_cancel_kb(), + ) + + await state.set_state(AdminServersEditor.waiting_for_subscription_url) -@router.message(UserEditorState.waiting_for_subscription_url, IsAdminFilter()) -async def handle_subscription_url_input(message: types.Message, state: FSMContext): - subscription_url = message.text.strip() - - if subscription_url == "❌ Отменить": - await state.clear() - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔧 Управление кластерами", callback_data="servers_editor" - ) - ) +@router.message( + AdminServersEditor.waiting_for_subscription_url, + IsAdminFilter() +) +async def handle_subscription_url_input( + message: types.Message, + state: FSMContext +): + if not message.text or not message.text.strip().startswith("https://"): await message.answer( - "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", - reply_markup=builder.as_markup(), - ) - return - - if not subscription_url.startswith("https://"): - await message.answer( - "❌ subscription_url должен начинаться с https://. Попробуйте снова.", + text="❌ subscription_url должен начинаться с https://. Попробуйте снова.", parse_mode="HTML", ) return - subscription_url = subscription_url.rstrip("/") + subscription_url = message.text.strip().rstrip("/") user_data = await state.get_data() cluster_name = user_data.get("cluster_name") server_name = user_data.get("server_name") await state.update_data(subscription_url=subscription_url) - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") + text = ( + f"Введите inbound_id для сервера {server_name} в кластере {cluster_name}:\n\n" + "Это номер подключения vless в вашей панели 3x-ui. Обычно это 1 при чистой настройке по гайду.\n\n" ) await message.answer( - f"Введите inbound_id для сервера {server_name} в кластере {cluster_name}:\n\n" - "Это номер подключения vless в вашей панели 3x-ui. Обычно это 1 при чистой настройке по гайду.\n\n", + text=text, parse_mode="HTML", - reply_markup=builder.as_markup(), + reply_markup=build_admin_back_kb("servers_editor"), ) - await state.set_state(UserEditorState.waiting_for_inbound_id) + await state.set_state(AdminServersEditor.waiting_for_inbound_id) -@router.message(UserEditorState.waiting_for_inbound_id, IsAdminFilter()) -async def handle_inbound_id_input(message: types.Message, state: FSMContext): +@router.message( + AdminServersEditor.waiting_for_inbound_id, + IsAdminFilter() +) +async def handle_inbound_id_input( + message: types.Message, + state: FSMContext +): inbound_id = message.text.strip() if not inbound_id.isdigit(): await message.answer( - "❌ inbound_id должен быть числовым значением. Попробуйте снова." + text="❌ inbound_id должен быть числовым значением. Попробуйте снова." ) return @@ -280,155 +254,62 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext): ) await conn.close() - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔙 Назад к кластерам", callback_data="servers_editor" - ) - ) - await message.answer( - f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!", - reply_markup=builder.as_markup(), + text=f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!", + reply_markup=build_admin_back_kb("servers_editor"), ) await state.clear() -@router.callback_query(F.data.startswith("manage_cluster|"), IsAdminFilter()) -async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMContext): - cluster_name = callback_query.data.split("|")[1] +@router.callback_query( + AdminServerEditorCallback.filter(F.action == "clusters_manage"), + IsAdminFilter() +) +async def handle_manage_cluster( + callback_query: types.CallbackQuery, + callback_data: AdminServerEditorCallback, +): + cluster_name = callback_data.data servers = await get_servers_from_db() cluster_servers = servers.get(cluster_name, []) - builder = InlineKeyboardBuilder() - - for server in cluster_servers: - builder.row( - InlineKeyboardButton( - text=f"🌍 {server['server_name']}", - callback_data=f"manage_server|{server['server_name']}", - ) - ) - - builder.row( - InlineKeyboardButton( - text="➕ Добавить сервер", callback_data=f"add_server|{cluster_name}" - ) - ) - - builder.row( - InlineKeyboardButton( - text="🌐 Доступность серверов", - callback_data=f"server_availability|{cluster_name}", - ) - ) - - builder.row( - InlineKeyboardButton( - text="💾 Создать бэкап кластера", - callback_data=f"backup_cluster|{cluster_name}", - ) - ) - - builder.row( - InlineKeyboardButton( - text="🔄 Синхронизировать", - callback_data=f"sync_cluster|{cluster_name}", - ) - ) - - builder.row( - InlineKeyboardButton( - text="🔙 Назад в управление кластерами", callback_data="servers_editor" - ) - ) - await callback_query.message.answer( - f"🔧 Управление серверами для кластера {cluster_name}", - reply_markup=builder.as_markup(), + text=f"🔧 Управление серверами для кластера {cluster_name}", + reply_markup=build_manage_cluster_kb(cluster_servers, cluster_name), ) -@router.callback_query(F.data.startswith("sync_cluster|"), IsAdminFilter()) -async def sync_cluster_handler(callback_query: types.CallbackQuery): - """Обработчик для синхронизации ключей на всех серверах выбранного кластера.""" - cluster_name = callback_query.data.split("|")[1] - - conn = await asyncpg.connect(DATABASE_URL) - try: - query_keys = """ - SELECT tg_id, client_id, email, expiry_time - FROM keys - WHERE server_id = $1 - """ - keys_to_sync = await conn.fetch(query_keys, cluster_name) - - if not keys_to_sync: - await callback_query.message.answer( - f"❌ Нет ключей для синхронизации в кластере {cluster_name}.", - reply_markup=InlineKeyboardBuilder() - .row( - InlineKeyboardButton( - text="🔙 Назад", callback_data="servers_editor" - ) - ) - .as_markup(), - ) - return - - tasks = [] - - for key in keys_to_sync: - tasks.append( - asyncio.create_task( - create_key_on_cluster( - cluster_name, - key["tg_id"], - key["client_id"], - key["email"], - key["expiry_time"], - ) - ) - ) - - await asyncio.gather(*tasks) - await callback_query.message.answer( - f"✅ Ключи успешно синхронизированы для кластера {cluster_name}.", - reply_markup=InlineKeyboardBuilder() - .row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor")) - .as_markup(), - ) - except Exception as e: - logger.error(f"Ошибка синхронизации ключей в кластере {cluster_name}: {e}") - await callback_query.message.answer( - f"❌ Произошла ошибка при синхронизации: {e}", - reply_markup=InlineKeyboardBuilder() - .row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor")) - .as_markup(), - ) - finally: - await conn.close() - - -@router.callback_query(F.data.startswith("server_availability|"), IsAdminFilter()) -async def handle_check_server_availability(callback_query: types.CallbackQuery): - cluster_name = callback_query.data.split("|")[1] +@router.callback_query( + AdminServerEditorCallback.filter(F.action == "servers_availability"), + IsAdminFilter() +) +async def handle_check_server_availability( + callback_query: types.CallbackQuery, + callback_data: AdminServerEditorCallback +): + cluster_name = callback_data.data servers = await get_servers_from_db() cluster_servers = servers.get(cluster_name, []) if not cluster_servers: - await callback_query.answer(f"Кластер '{cluster_name}' не содержит серверов.") + await callback_query.answer( + text=f"Кластер '{cluster_name}' не содержит серверов." + ) return - in_progress_message = await callback_query.message.answer( + text = ( f"🖥️ Проверка доступности серверов для кластера {cluster_name}.\n\n" "Это может занять до 1 минуты, пожалуйста, подождите..." ) - availability_message = ( + in_progress_message = await callback_query.message.answer( + text=text + ) + + text = ( f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n" ) @@ -441,97 +322,84 @@ async def handle_check_server_availability(callback_query: types.CallbackQuery): await xui.login() online_users = len(await xui.client.online()) - availability_message += ( + text += ( f"🌍 {server['server_name']}: {online_users} активных пользователей.\n" ) except Exception as e: - availability_message += f"❌ {server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n" - - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}" - ) - ) + text += f"❌ {server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n" await in_progress_message.edit_text( - availability_message, reply_markup=builder.as_markup() + text=text, + reply_markup=build_admin_back_kb("servers_editor") ) await callback_query.answer() -@router.callback_query(F.data.startswith("manage_server|"), IsAdminFilter()) -async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMContext): - server_name = callback_query.data.split("|")[1] - +@router.callback_query( + AdminServerEditorCallback.filter(F.action == "servers_manage"), + IsAdminFilter() +) +async def handle_manage_server( + callback_query: types.CallbackQuery, + callback_data: AdminServerEditorCallback +): + server_name = callback_data.data servers = await get_servers_from_db() - server = None - cluster_name = None - for cluster, cluster_servers in servers.items(): - server = next( - (s for s in cluster_servers if s["server_name"] == server_name), None - ) - if server: - cluster_name = cluster - break + cluster_name, server = next( + ((c, s) for c, cs in servers.items() + for s in cs if s["server_name"] == server_name), + (None, None) + ) if server: api_url = server["api_url"] subscription_url = server["subscription_url"] inbound_id = server["inbound_id"] - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🗑️ Удалить", callback_data=f"delete_server|{server_name}" - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}" - ) - ) - - await callback_query.message.answer( + text = ( f"🔧 Информация о сервере {server_name}:\n\n" f"📡 API URL: {api_url}\n" f"🌐 Subscription URL: {subscription_url}\n" - f"🔑 Inbound ID: {inbound_id}", + f"🔑 Inbound ID: {inbound_id}" + ) + + await callback_query.message.answer( + text=text, parse_mode="HTML", - reply_markup=builder.as_markup(), + reply_markup=build_manage_server_kb(server_name, cluster_name), ) else: await callback_query.message.answer("❌ Сервер не найден.") -@router.callback_query(F.data.startswith("delete_server|"), IsAdminFilter()) -async def handle_delete_server(callback_query: types.CallbackQuery, state: FSMContext): - server_name = callback_query.data.split("|")[1] - - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="✅ Да", callback_data=f"confirm_delete_server|{server_name}" - ), - InlineKeyboardButton( - text="❌ Нет", callback_data=f"manage_server|{server_name}" - ), - ) +@router.callback_query( + AdminServerEditorCallback.filter(F.action == "servers_delete"), + IsAdminFilter() +) +async def handle_delete_server( + callback_query: types.CallbackQuery, + callback_data: AdminServerEditorCallback +): + server_name = callback_data.data await callback_query.message.answer( - f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?", - reply_markup=builder.as_markup(), + text=f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?", + reply_markup=build_delete_server_kb(server_name), ) -@router.callback_query(F.data.startswith("confirm_delete_server|"), IsAdminFilter()) +@router.callback_query( + AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"), + IsAdminFilter() +) async def handle_confirm_delete_server( - callback_query: types.CallbackQuery, state: FSMContext + callback_query: types.CallbackQuery, + callback_data: AdminServerEditorCallback ): - server_name = callback_query.data.split("|")[1] + server_name = callback_data.data conn = await asyncpg.connect(DATABASE_URL) await conn.execute( @@ -542,43 +410,49 @@ async def handle_confirm_delete_server( ) await conn.close() - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔙 Назад в управление кластерами", callback_data="servers_editor" - ) - ) - await callback_query.message.answer( - f"🗑️ Сервер {server_name} успешно удален.", reply_markup=builder.as_markup() + text=f"🗑️ Сервер {server_name} успешно удален.", + reply_markup=build_admin_back_kb("servers_editor") ) -@router.callback_query(F.data.startswith("add_server|"), IsAdminFilter()) -async def handle_add_server(callback_query: types.CallbackQuery, state: FSMContext): - cluster_name = callback_query.data.split("|")[1] +@router.callback_query( + AdminServerEditorCallback.filter(F.action == "servers_add"), + IsAdminFilter() +) +async def handle_add_server( + callback_query: types.CallbackQuery, + callback_data: AdminServerEditorCallback, + state: FSMContext +): + cluster_name = callback_data.data await state.update_data(cluster_name=cluster_name) - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") + text = ( + f"Введите имя сервера для кластера {cluster_name}:\n\n" + "Рекомендуется указать локацию сервера в имени.\n\n" + "Пример: server-asia, server-europe" ) await callback_query.message.answer( - f"Введите имя сервера для кластера {cluster_name}:\n\n" - "Рекомендуется указать локацию сервера в имени.\n\n" - "Пример: server-asia, server-europe", + text=text, parse_mode="HTML", - reply_markup=builder.as_markup(), + reply_markup=build_admin_back_kb("servers_editor"), ) - await state.set_state(UserEditorState.waiting_for_server_name) + await state.set_state(AdminServersEditor.waiting_for_server_name) -@router.callback_query(F.data.startswith("backup_cluster|"), IsAdminFilter()) -async def handle_backup_cluster(callback_query: types.CallbackQuery): - cluster_name = callback_query.data.split("|")[1] +@router.callback_query( + AdminServerEditorCallback.filter(F.action == "clusters_backup"), + IsAdminFilter() +) +async def handle_backup_cluster( + callback_query: types.CallbackQuery, + callback_data: AdminServerEditorCallback, +): + cluster_name = callback_data.data servers = await get_servers_from_db() cluster_servers = servers.get(cluster_name, []) @@ -591,13 +465,14 @@ async def handle_backup_cluster(callback_query: types.CallbackQuery): ) await create_backup_and_send_to_admins(xui) - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor")) + text = ( + f"Бэкап для кластера {cluster_name} был успешно создан и отправлен администраторам!\n\n" + f"🔔 Бэкапы отправлены в боты панелей." + ) await callback_query.message.answer( - f"Бэкап для кластера {cluster_name} был успешно создан и отправлен администраторам!\n\n" - f"🔔 Бэкапы отправлены в боты панелей.", + text=text, parse_mode="HTML", - reply_markup=builder.as_markup(), + reply_markup=build_admin_back_kb("servers_editor"), ) await callback_query.answer() diff --git a/keyboards/admin/servers_kb.py b/keyboards/admin/servers_kb.py new file mode 100644 index 00000000..702feed9 --- /dev/null +++ b/keyboards/admin/servers_kb.py @@ -0,0 +1,120 @@ +from aiogram.filters.callback_data import CallbackData +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn + + +class AdminServerEditorCallback(CallbackData, prefix='admin_server_editor'): + action: str + data: str + + +def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + for cluster_name in servers: + builder.button( + text=f"⚙️ {cluster_name}", + callback_data=AdminServerEditorCallback( + action="clusters_manage", + data=cluster_name + ).pack() + ) + + builder.button( + text="➕ Добавить кластер", + callback_data=AdminPanelCallback(action="clusters_add").pack() + ) + builder.row( + build_admin_back_btn("admin") + ) + return builder.as_markup() + + +def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + for server in cluster_servers: + builder.button( + text=f"🌍 {server['server_name']}", + callback_data=AdminServerEditorCallback( + action="servers_manage", + data=server["server_name"] + ).pack() + ) + + builder.button( + text="➕ Добавить сервер", + callback_data=AdminServerEditorCallback( + action="servers_add", + data=cluster_name + ).pack() + ) + builder.button( + text="🌐 Доступность серверов", + callback_data=AdminServerEditorCallback( + action="servers_availability", + data=cluster_name + ).pack() + ) + builder.button( + text="💾 Создать бэкап кластера", + callback_data=AdminServerEditorCallback( + action="clusters_backup", + data=cluster_name + ).pack() + ) + builder.row( + build_admin_back_btn("servers_editor") + ) + return builder.as_markup() + + +def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button( + text="🗑️ Удалить", + callback_data=AdminServerEditorCallback( + action="servers_delete", + data=server_name + ).pack() + ) + builder.button( + text="🔙 Назад", + callback_data=AdminServerEditorCallback( + action="clusters_manage", + data=cluster_name + ).pack() + ) + return builder.as_markup() + + +def build_delete_server_kb(server_name: str) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button( + text="✅ Да", + callback_data=AdminServerEditorCallback( + action="servers_delete_confirm", + data=server_name + ).pack() + ) + builder.button( + text="🔙 Назад", + callback_data=AdminServerEditorCallback( + action="servers_manage", + data=server_name + ).pack() + ) + return builder.as_markup() + + +def build_cancel_kb() -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton( + text="❌ Отменить", + callback_data="servers_editor" + ) + ) + return builder.as_markup() From f11b014acfc5ff655d0abafedb334d905cd2bc74 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 12 Jan 2025 17:29:48 +0300 Subject: [PATCH 11/23] Implement admin sender handler --- handlers/admin/admin_sender.py | 105 +++++++++++++++++++++++++++++++++ keyboards/admin/sender_kb.py | 35 +++++++++++ 2 files changed, 140 insertions(+) create mode 100644 handlers/admin/admin_sender.py create mode 100644 keyboards/admin/sender_kb.py diff --git a/handlers/admin/admin_sender.py b/handlers/admin/admin_sender.py new file mode 100644 index 00000000..101aaf86 --- /dev/null +++ b/handlers/admin/admin_sender.py @@ -0,0 +1,105 @@ +from datetime import datetime +from typing import Any + +from aiogram import F, Router, types +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from aiogram.types import CallbackQuery + +from bot import bot +from filters.admin import IsAdminFilter +from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb +from keyboards.admin.sender_kb import build_sender_kb, AdminSenderCallback +from logger import logger + +router = Router() + + +class AdminSender(StatesGroup): + waiting_for_message = State() + + +@router.callback_query( + AdminPanelCallback.filter(F.action == "sender"), + IsAdminFilter(), +) +async def handle_sender( + callback_query: CallbackQuery +): + await callback_query.message.answer( + text="✍️ Выберите группу пользователей для рассылки:", + reply_markup=build_sender_kb(), + ) + + +@router.callback_query( + AdminSenderCallback, + IsAdminFilter(), +) +async def handle_sender_callback( + callback_query: CallbackQuery, + callback_data: AdminSenderCallback, + state: FSMContext +): + await callback_query.message.answer( + text="✍️ Введите текст сообщения для рассылки:", + reply_markup=build_admin_back_kb("sender"), + ) + await state.update_data(type=callback_data.type) + await state.set_state(AdminSender.waiting_for_message) + + +@router.message( + AdminSender.waiting_for_message, + IsAdminFilter(), +) +async def process_message( + message: types.Message, + state: FSMContext, + session: Any +): + text_message = message.text + + try: + state_data = await state.get_data() + send_to = state_data.get("type", "all") + + if send_to == "subscribed": + tg_ids = await session.fetch(""" + SELECT DISTINCT c.tg_id + FROM connections c + JOIN keys k ON c.tg_id = k.tg_id + WHERE k.expiry_time > $1 + """, int(datetime.utcnow().timestamp() * 1000)) + elif send_to == "unsubscribed": + tg_ids = await session.fetch(""" + SELECT c.tg_id + FROM connections c + LEFT JOIN keys k ON c.tg_id = k.tg_id + GROUP BY c.tg_id + HAVING COUNT(k.tg_id) = 0 OR MAX(k.expiry_time) <= $1 + """, int(datetime.utcnow().timestamp() * 1000)) + else: + tg_ids = await session.fetch("SELECT DISTINCT tg_id FROM connections") + + total_users = len(tg_ids) + success_count = 0 + + for record in tg_ids: + tg_id = record["tg_id"] + try: + await bot.send_message(chat_id=tg_id, text=text_message) + success_count += 1 + except Exception: + pass + + await message.answer( + f"📤 Рассылка завершена:\n" + f"👥 Всего пользователей: {total_users}\n" + f"✅ Доставлено: {success_count}\n" + f"❌ Не доставлено: {total_users - success_count}" + ) + except Exception as e: + logger.error(f"❗ Ошибка при подключении к базе данных: {e}") + + await state.clear() diff --git a/keyboards/admin/sender_kb.py b/keyboards/admin/sender_kb.py new file mode 100644 index 00000000..895e4fbb --- /dev/null +++ b/keyboards/admin/sender_kb.py @@ -0,0 +1,35 @@ +from aiogram.filters.callback_data import CallbackData +from aiogram.types import InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from keyboards.admin.panel_kb import build_admin_back_btn + + +class AdminSenderCallback(CallbackData, prefix='admin_sender'): + type: str + + +def build_sender_kb() -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button( + text="📢 Отправить всем", + callback_data=AdminSenderCallback( + type="all" + ).pack() + ) + builder.button( + text="📢 Отправить с подпиской", + callback_data=AdminSenderCallback( + type="subscribed" + ).pack() + ) + builder.button( + text="📢 Отправить без подписки", + callback_data=AdminSenderCallback( + type="unsubscribed" + ).pack() + ) + builder.row( + build_admin_back_btn("admin") + ) + return builder.as_markup() From 054d710d6c8c572a6f9c6f573fb6d41b3a8a5208 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 12 Jan 2025 17:32:11 +0300 Subject: [PATCH 12/23] Register all the missing routers --- handlers/admin/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py index 549705b6..1dffebf9 100644 --- a/handlers/admin/__init__.py +++ b/handlers/admin/__init__.py @@ -2,9 +2,13 @@ __all__ = ("router",) from aiogram import Router +from .admin_backups import router as backups_router from .admin_coupons import router as coupons_router from .admin_panel import router as panel_router +from .admin_restart import router as restart_router +from .admin_sender import router as sender_router from .admin_servers import router as servers_router +from .admin_stats import router as stats_router from .admin_users import router as users_router router = Router(name="admins_main_router") @@ -12,6 +16,10 @@ router = Router(name="admins_main_router") router.include_routers( panel_router, servers_router, - coupons_router, users_router, + stats_router, + backups_router, + sender_router, + coupons_router, + restart_router, ) From 7c19492c051d47023a34059e0fb5671e195e3d95 Mon Sep 17 00:00:00 2001 From: hteppl Date: Sun, 12 Jan 2025 17:35:42 +0300 Subject: [PATCH 13/23] Fix some typo and quotes --- keyboards/admin/coupons_kb.py | 14 ++++++-------- keyboards/admin/panel_kb.py | 2 +- keyboards/admin/sender_kb.py | 2 +- keyboards/admin/servers_kb.py | 2 +- keyboards/admin/users_kb.py | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/keyboards/admin/coupons_kb.py b/keyboards/admin/coupons_kb.py index 25467a16..99277f1d 100644 --- a/keyboards/admin/coupons_kb.py +++ b/keyboards/admin/coupons_kb.py @@ -2,10 +2,10 @@ from aiogram.filters.callback_data import CallbackData from aiogram.types import InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder -from keyboards.admin.panel_kb import AdminPanelCallback +from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn -class AdminCouponDeleteCallback(CallbackData, prefix='admin_coupon_delete'): +class AdminCouponDeleteCallback(CallbackData, prefix="admin_coupon_delete"): coupon_code: str @@ -19,9 +19,8 @@ def build_coupons_kb() -> InlineKeyboardMarkup: text="Купоны", callback_data=AdminPanelCallback(action="coupons_list").pack() ) - builder.button( - text="🔙 Назад", - callback_data="admin" + builder.row( + build_admin_back_btn("admin") ) return builder.as_markup() @@ -36,8 +35,7 @@ def build_coupons_list_kb(coupons: list) -> InlineKeyboardMarkup: callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code), ) - builder.button( - text="🔙 Назад", - callback_data=AdminPanelCallback(action="coupons").pack() + builder.row( + build_admin_back_btn("coupons") ) return builder.as_markup() diff --git a/keyboards/admin/panel_kb.py b/keyboards/admin/panel_kb.py index 93971469..56cff519 100644 --- a/keyboards/admin/panel_kb.py +++ b/keyboards/admin/panel_kb.py @@ -3,7 +3,7 @@ from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -class AdminPanelCallback(CallbackData, prefix='admin_panel'): +class AdminPanelCallback(CallbackData, prefix="admin_panel"): action: str diff --git a/keyboards/admin/sender_kb.py b/keyboards/admin/sender_kb.py index 895e4fbb..502c8c4b 100644 --- a/keyboards/admin/sender_kb.py +++ b/keyboards/admin/sender_kb.py @@ -5,7 +5,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from keyboards.admin.panel_kb import build_admin_back_btn -class AdminSenderCallback(CallbackData, prefix='admin_sender'): +class AdminSenderCallback(CallbackData, prefix="admin_sender"): type: str diff --git a/keyboards/admin/servers_kb.py b/keyboards/admin/servers_kb.py index 702feed9..e598356c 100644 --- a/keyboards/admin/servers_kb.py +++ b/keyboards/admin/servers_kb.py @@ -5,7 +5,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn -class AdminServerEditorCallback(CallbackData, prefix='admin_server_editor'): +class AdminServerEditorCallback(CallbackData, prefix="admin_servers"): action: str data: str diff --git a/keyboards/admin/users_kb.py b/keyboards/admin/users_kb.py index f93e2dcd..062d81e4 100644 --- a/keyboards/admin/users_kb.py +++ b/keyboards/admin/users_kb.py @@ -5,7 +5,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from keyboards.admin.panel_kb import build_admin_back_btn -class AdminUserEditorCallback(CallbackData, prefix='admin_users_editor'): +class AdminUserEditorCallback(CallbackData, prefix="admin_users"): action: str data: str From 55b065f78448fa23e83b641537c7f9d9b9a6d0d8 Mon Sep 17 00:00:00 2001 From: hteppl Date: Wed, 15 Jan 2025 03:37:01 +0300 Subject: [PATCH 14/23] Admin keyboards adjustments --- keyboards/admin/coupons_kb.py | 4 +++- keyboards/admin/panel_kb.py | 3 +++ keyboards/admin/sender_kb.py | 1 + keyboards/admin/servers_kb.py | 9 +++++++-- keyboards/admin/stats_kb.py | 1 + keyboards/admin/users_kb.py | 4 ++++ 6 files changed, 19 insertions(+), 3 deletions(-) diff --git a/keyboards/admin/coupons_kb.py b/keyboards/admin/coupons_kb.py index 99277f1d..0d024a05 100644 --- a/keyboards/admin/coupons_kb.py +++ b/keyboards/admin/coupons_kb.py @@ -32,7 +32,9 @@ def build_coupons_list_kb(coupons: list) -> InlineKeyboardMarkup: coupon_code = coupon["code"] builder.button( text=f"❌ Удалить {coupon_code}", - callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code), + callback_data=AdminCouponDeleteCallback( + coupon_code=coupon_code + ).pack(), ) builder.row( diff --git a/keyboards/admin/panel_kb.py b/keyboards/admin/panel_kb.py index 56cff519..2786dbd5 100644 --- a/keyboards/admin/panel_kb.py +++ b/keyboards/admin/panel_kb.py @@ -43,6 +43,7 @@ def build_panel_kb() -> InlineKeyboardMarkup: text="👤 Личный кабинет", callback_data="profile" ) + builder.adjust(1, 1, 2, 1) return builder.as_markup() @@ -63,6 +64,7 @@ def build_management_kb() -> InlineKeyboardMarkup: builder.row( build_admin_back_btn("admin") ) + builder.adjust(1) return builder.as_markup() @@ -75,6 +77,7 @@ def build_restart_kb() -> InlineKeyboardMarkup: builder.row( build_admin_back_btn("admin") ) + builder.adjust(1) return builder.as_markup() diff --git a/keyboards/admin/sender_kb.py b/keyboards/admin/sender_kb.py index 502c8c4b..5d9155a7 100644 --- a/keyboards/admin/sender_kb.py +++ b/keyboards/admin/sender_kb.py @@ -32,4 +32,5 @@ def build_sender_kb() -> InlineKeyboardMarkup: builder.row( build_admin_back_btn("admin") ) + builder.adjust(1) return builder.as_markup() diff --git a/keyboards/admin/servers_kb.py b/keyboards/admin/servers_kb.py index e598356c..dc974c92 100644 --- a/keyboards/admin/servers_kb.py +++ b/keyboards/admin/servers_kb.py @@ -29,6 +29,7 @@ def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup: builder.row( build_admin_back_btn("admin") ) + builder.adjust(1) return builder.as_markup() @@ -66,8 +67,9 @@ def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMark ).pack() ) builder.row( - build_admin_back_btn("servers_editor") + build_admin_back_btn("servers") ) + builder.adjust(1) return builder.as_markup() @@ -87,6 +89,7 @@ def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboar data=cluster_name ).pack() ) + builder.adjust(1) return builder.as_markup() @@ -106,6 +109,7 @@ def build_delete_server_kb(server_name: str) -> InlineKeyboardMarkup: data=server_name ).pack() ) + builder.adjust(1) return builder.as_markup() @@ -114,7 +118,8 @@ def build_cancel_kb() -> InlineKeyboardMarkup: builder.row( InlineKeyboardButton( text="❌ Отменить", - callback_data="servers_editor" + callback_data="servers" ) ) + builder.adjust(1) return builder.as_markup() diff --git a/keyboards/admin/stats_kb.py b/keyboards/admin/stats_kb.py index 3e3b9b04..c6b34404 100644 --- a/keyboards/admin/stats_kb.py +++ b/keyboards/admin/stats_kb.py @@ -21,4 +21,5 @@ def build_stats_kb() -> InlineKeyboardMarkup: builder.row( build_admin_back_btn("admin") ) + builder.adjust(1) return builder.as_markup() diff --git a/keyboards/admin/users_kb.py b/keyboards/admin/users_kb.py index 062d81e4..57d7941c 100644 --- a/keyboards/admin/users_kb.py +++ b/keyboards/admin/users_kb.py @@ -46,6 +46,7 @@ def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup: builder.row( build_admin_back_btn("users_editor") ) + builder.adjust(1) return builder.as_markup() @@ -61,6 +62,7 @@ def build_user_delete_kb(tg_id: int): builder.row( build_admin_back_btn("users_editor") ) + builder.adjust(1) return builder.as_markup() @@ -96,6 +98,7 @@ def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup: builder.row( build_admin_back_btn("users_editor") ) + builder.adjust(1) return builder.as_markup() @@ -113,4 +116,5 @@ def build_key_delete_kb(client_id: str) -> InlineKeyboardMarkup: builder.row( build_admin_back_btn("users_editor") ) + builder.adjust(1) return builder.as_markup() From 0092764f8f7723247ff66adb2041cffcdb107330 Mon Sep 17 00:00:00 2001 From: hteppl Date: Wed, 15 Jan 2025 07:58:46 +0300 Subject: [PATCH 15/23] Implement banned by users handler --- handlers/admin/__init__.py | 2 + handlers/admin/admin_bans.py | 107 +++++++++++++++++++++++++++++++++++ keyboards/admin/bans_kb.py | 21 +++++++ 3 files changed, 130 insertions(+) create mode 100644 handlers/admin/admin_bans.py create mode 100644 keyboards/admin/bans_kb.py diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py index 1dffebf9..cb91e6f3 100644 --- a/handlers/admin/__init__.py +++ b/handlers/admin/__init__.py @@ -3,6 +3,7 @@ __all__ = ("router",) from aiogram import Router from .admin_backups import router as backups_router +from .admin_bans import router as bans_router from .admin_coupons import router as coupons_router from .admin_panel import router as panel_router from .admin_restart import router as restart_router @@ -22,4 +23,5 @@ router.include_routers( sender_router, coupons_router, restart_router, + bans_router, ) diff --git a/handlers/admin/admin_bans.py b/handlers/admin/admin_bans.py new file mode 100644 index 00000000..a515a14e --- /dev/null +++ b/handlers/admin/admin_bans.py @@ -0,0 +1,107 @@ +from typing import Any + +from aiogram import F, Router +from aiogram.types import CallbackQuery, BufferedInputFile + +from database import delete_user_data +from filters.admin import IsAdminFilter +from keyboards.admin.bans_kb import build_bans_kb +from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb + +router = Router() + + +@router.callback_query( + AdminPanelCallback.filter(F.action == "bans"), + IsAdminFilter(), +) +async def handle_bans( + callback_query: CallbackQuery +): + text = ( + "🚫 Заблокировавшие бота" + "\n\nЗдесь можно просматривать и удалять пользователей, которые забанили вашего бота!" + ) + + await callback_query.message.edit_text( + text=text, + reply_markup=build_bans_kb(), + ) + + +@router.callback_query( + AdminPanelCallback.filter(F.action == "bans_export"), + IsAdminFilter(), +) +async def handle_bans_export( + callback_query: CallbackQuery, + session: Any +): + kb = build_admin_back_kb("management") + + try: + banned_users = await session.fetch("SELECT tg_id, blocked_at FROM blocked_users") + + import csv + import io + + csv_output = io.StringIO() + writer = csv.writer(csv_output) + writer.writerow(["tg_id", "blocked_at"]) + for user in banned_users: + writer.writerow([user["tg_id"], user["blocked_at"]]) + + csv_output.seek(0) + + document = BufferedInputFile( + file=csv_output.getvalue().encode("utf-8"), filename="banned_users.csv" + ) + + await callback_query.message.answer_document( + document=document, + caption="📥 Экспорт пользователей, заблокировавших бота в CSV", + ) + except Exception as e: + await callback_query.message.answer( + text=f"❗ Произошла ошибка при экспорте: {e}", + reply_markup=kb, + ) + + +@router.callback_query( + AdminPanelCallback.filter(F.action == "bans_delete_banned"), + IsAdminFilter(), +) +async def handle_bans_delete_banned( + callback_query: CallbackQuery, + session: Any +): + kb = build_admin_back_kb("bans") + + try: + blocked_users = await session.fetch("SELECT tg_id FROM blocked_users") + blocked_ids = [record["tg_id"] for record in blocked_users] + + if not blocked_ids: + await callback_query.message.answer( + text="📂 Нет заблокировавших пользователей для удаления.", + reply_markup=kb, + ) + return + + for tg_id in blocked_ids: + await delete_user_data(session, tg_id) + + await session.execute( + "DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids + ) + + await callback_query.message.answer( + text=f"🗑️ Удалены данные о {len(blocked_ids)} пользователях и связанных записях.", + reply_markup=kb, + ) + except Exception as e: + await callback_query.message.answer( + text=f"❗ Произошла ошибка при удалении записей: {e}", + reply_markup=kb, + ) diff --git a/keyboards/admin/bans_kb.py b/keyboards/admin/bans_kb.py new file mode 100644 index 00000000..958d0a14 --- /dev/null +++ b/keyboards/admin/bans_kb.py @@ -0,0 +1,21 @@ +from aiogram.types import InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from keyboards.admin.panel_kb import build_admin_back_btn, AdminPanelCallback + + +def build_bans_kb() -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button( + text="📄 Выгрузить в CSV", + callback_data=AdminPanelCallback(action="bans_export").pack() + ) + builder.button( + text="🗑️ Удалить из БД", + callback_data=AdminPanelCallback(action="bans_delete_banned").pack() + ) + builder.row( + build_admin_back_btn("management") + ) + builder.adjust(1) + return builder.as_markup() From a02062a4b40a9827b5f62afb8b992fa9761a534c Mon Sep 17 00:00:00 2001 From: hteppl Date: Wed, 15 Jan 2025 08:00:41 +0300 Subject: [PATCH 16/23] Optimize and fix keyboards --- keyboards/admin/coupons_kb.py | 2 +- keyboards/admin/panel_kb.py | 34 ++++++----- keyboards/admin/sender_kb.py | 8 +-- keyboards/admin/servers_kb.py | 2 +- keyboards/admin/stats_kb.py | 2 +- keyboards/admin/users_kb.py | 111 +++++++++++++++++++++------------- 6 files changed, 94 insertions(+), 65 deletions(-) diff --git a/keyboards/admin/coupons_kb.py b/keyboards/admin/coupons_kb.py index 0d024a05..41ebd8c3 100644 --- a/keyboards/admin/coupons_kb.py +++ b/keyboards/admin/coupons_kb.py @@ -20,7 +20,7 @@ def build_coupons_kb() -> InlineKeyboardMarkup: callback_data=AdminPanelCallback(action="coupons_list").pack() ) builder.row( - build_admin_back_btn("admin") + build_admin_back_btn() ) return builder.as_markup() diff --git a/keyboards/admin/panel_kb.py b/keyboards/admin/panel_kb.py index 2786dbd5..9b85a216 100644 --- a/keyboards/admin/panel_kb.py +++ b/keyboards/admin/panel_kb.py @@ -10,7 +10,7 @@ class AdminPanelCallback(CallbackData, prefix="admin_panel"): def build_panel_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( - text="🔍 Поиск пользователя", + text="👤 Поиск пользователя", callback_data=AdminPanelCallback(action="search_user").pack() ) builder.button( @@ -31,19 +31,21 @@ def build_panel_kb() -> InlineKeyboardMarkup: text="📢 Рассылка", callback_data=AdminPanelCallback(action="sender").pack() ) - builder.button( - text="🤖 Управление Ботом", - callback_data=AdminPanelCallback(action="management").pack() + builder.row( + InlineKeyboardButton( + text="📊 Статистика", + callback_data=AdminPanelCallback(action="stats").pack() + ), + InlineKeyboardButton( + text="🤖 Управление", + callback_data=AdminPanelCallback(action="management").pack() + ) ) builder.button( - text="📊 Статистика", - callback_data=AdminPanelCallback(action="stats").pack() - ) - builder.button( - text="👤 Личный кабинет", + text="Личный кабинет", callback_data="profile" ) - builder.adjust(1, 1, 2, 1) + builder.adjust(1, 1, 2, 1, 2, 1) return builder.as_markup() @@ -54,7 +56,7 @@ def build_management_kb() -> InlineKeyboardMarkup: callback_data=AdminPanelCallback(action="backups").pack() ) builder.button( - text="🚫 Баны", + text="🚫 Заблокировавшие бота", callback_data=AdminPanelCallback(action="bans").pack() ) builder.button( @@ -62,7 +64,7 @@ def build_management_kb() -> InlineKeyboardMarkup: callback_data=AdminPanelCallback(action="restart").pack() ) builder.row( - build_admin_back_btn("admin") + build_admin_back_btn() ) builder.adjust(1) return builder.as_markup() @@ -71,17 +73,17 @@ def build_management_kb() -> InlineKeyboardMarkup: def build_restart_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( - text="✅ Да, перезапустить", + text="✅ Да, перезагрузить", callback_data=AdminPanelCallback(action="restart_confirm").pack() ) builder.row( - build_admin_back_btn("admin") + build_admin_back_btn() ) builder.adjust(1) return builder.as_markup() -def build_admin_back_kb(action: str) -> InlineKeyboardMarkup: +def build_admin_back_kb(action: str = "admin") -> InlineKeyboardMarkup: return build_admin_singleton_kb("🔙 Назад", action) @@ -93,7 +95,7 @@ def build_admin_singleton_kb(text: str, action: str) -> InlineKeyboardMarkup: return builder.as_markup() -def build_admin_back_btn(action: str) -> InlineKeyboardMarkup: +def build_admin_back_btn(action: str = "admin") -> InlineKeyboardMarkup: return build_admin_btn("🔙 Назад", action) diff --git a/keyboards/admin/sender_kb.py b/keyboards/admin/sender_kb.py index 5d9155a7..bfe9ad39 100644 --- a/keyboards/admin/sender_kb.py +++ b/keyboards/admin/sender_kb.py @@ -12,25 +12,25 @@ class AdminSenderCallback(CallbackData, prefix="admin_sender"): def build_sender_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( - text="📢 Отправить всем", + text="👥 Все пользователи", callback_data=AdminSenderCallback( type="all" ).pack() ) builder.button( - text="📢 Отправить с подпиской", + text="✅ Пользователи с подпиской", callback_data=AdminSenderCallback( type="subscribed" ).pack() ) builder.button( - text="📢 Отправить без подписки", + text="❌ Пользователи без подписки", callback_data=AdminSenderCallback( type="unsubscribed" ).pack() ) builder.row( - build_admin_back_btn("admin") + build_admin_back_btn() ) builder.adjust(1) return builder.as_markup() diff --git a/keyboards/admin/servers_kb.py b/keyboards/admin/servers_kb.py index dc974c92..e9e45140 100644 --- a/keyboards/admin/servers_kb.py +++ b/keyboards/admin/servers_kb.py @@ -27,7 +27,7 @@ def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup: callback_data=AdminPanelCallback(action="clusters_add").pack() ) builder.row( - build_admin_back_btn("admin") + build_admin_back_btn() ) builder.adjust(1) return builder.as_markup() diff --git a/keyboards/admin/stats_kb.py b/keyboards/admin/stats_kb.py index c6b34404..4e1db626 100644 --- a/keyboards/admin/stats_kb.py +++ b/keyboards/admin/stats_kb.py @@ -19,7 +19,7 @@ def build_stats_kb() -> InlineKeyboardMarkup: callback_data=AdminPanelCallback(action="stats_export_payments_csv").pack() ) builder.row( - build_admin_back_btn("admin") + build_admin_back_btn() ) builder.adjust(1) return builder.as_markup() diff --git a/keyboards/admin/users_kb.py b/keyboards/admin/users_kb.py index 57d7941c..0bd6cf86 100644 --- a/keyboards/admin/users_kb.py +++ b/keyboards/admin/users_kb.py @@ -7,44 +7,47 @@ from keyboards.admin.panel_kb import build_admin_back_btn class AdminUserEditorCallback(CallbackData, prefix="admin_users"): action: str - data: str + tg_id: int + data: str | None = None def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - for email in key_records: + for record in key_records: + email = record["email"] builder.button( text=f"🔑 {email}", callback_data=AdminUserEditorCallback( action="users_key_edit", - data=email + tg_id=tg_id, + data=str(email) ).pack() ) builder.button( - text="📝 Изменить баланс", + text="💸 Изменить баланс", callback_data=AdminUserEditorCallback( action="users_balance_change", - data=tg_id + tg_id=tg_id ).pack() ) builder.button( - text="🔄 Восстановить пробник", + text="🔄 Восстановить триал", callback_data=AdminUserEditorCallback( action="users_trial_restore", - data=tg_id + tg_id=tg_id ).pack() ) builder.button( text="❌ Удалить клиента", callback_data=AdminUserEditorCallback( action="users_delete_user", - data=tg_id + tg_id=tg_id ).pack() ) builder.row( - build_admin_back_btn("users_editor") + build_admin_back_btn() ) builder.adjust(1) return builder.as_markup() @@ -53,14 +56,14 @@ def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup: def build_user_delete_kb(tg_id: int): builder = InlineKeyboardBuilder() builder.button( - text="❌ Удалить клиента", + text="❌ Да, удалить!", callback_data=AdminUserEditorCallback( action="users_delete_user_confirm", - data=tg_id + tg_id=tg_id ).pack() ) builder.row( - build_admin_back_btn("users_editor") + build_editor_back_btn(tg_id, True) ) builder.adjust(1) return builder.as_markup() @@ -68,53 +71,77 @@ def build_user_delete_kb(tg_id: int): def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="ℹ️ Получить информацию о юзере", - callback_data=AdminUserEditorCallback( - action="users_info", - data=key_details["tg_id"] - ).pack() - ) + builder.button( + text="ℹ️ Получить информацию о юзере", + callback_data=AdminUserEditorCallback( + action="users_info", + tg_id=key_details["tg_id"] + ).pack() + ) + builder.button( + text="⏳ Изменить время истечения", + callback_data=AdminUserEditorCallback( + action="users_change_expiry", + data=email, + tg_id=key_details["tg_id"] + ).pack() + ) + builder.button( + text="❌ Удалить ключ", + callback_data=AdminUserEditorCallback( + action="users_delete_key", + data=email, + tg_id=key_details["tg_id"] + ).pack() ) builder.row( - InlineKeyboardButton( - text="⏳ Изменить время истечения", - callback_data=AdminUserEditorCallback( - action="users_change_expiry", - data=email - ).pack() - ) - ) - builder.row( - InlineKeyboardButton( - text="❌ Удалить ключ", - callback_data=AdminUserEditorCallback( - action="users_delete_key", - data=email - ).pack() - ) - ) - builder.row( - build_admin_back_btn("users_editor") + build_editor_back_btn(key_details["tg_id"], True) ) builder.adjust(1) return builder.as_markup() -def build_key_delete_kb(client_id: str) -> InlineKeyboardMarkup: +def build_key_delete_kb(tg_id: int, client_id: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( text="✅ Да, удалить", callback_data=AdminUserEditorCallback( action="users_delete_key_confirm", - data=client_id + data=client_id, + tg_id=tg_id ).pack() ) ) builder.row( - build_admin_back_btn("users_editor") + build_editor_back_btn(tg_id) ) builder.adjust(1) return builder.as_markup() + + +def build_editor_kb(tg_id: int, edit: bool = False) -> InlineKeyboardMarkup: + return build_editor_singleton_kb("🔙 Назад", tg_id, edit) + + +def build_editor_singleton_kb(text: str, tg_id: int, edit: bool = False) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.row( + build_editor_btn(text, tg_id, edit) + ) + return builder.as_markup() + + +def build_editor_back_btn(tg_id: int, edit: bool = False) -> InlineKeyboardButton: + return build_editor_btn("🔙 Назад", tg_id, edit) + + +def build_editor_btn(text: str, tg_id: int, edit: bool = False) -> InlineKeyboardButton: + return InlineKeyboardButton( + text=text, + callback_data=AdminUserEditorCallback( + action="users_editor", + data="" if edit else "edit", + tg_id=tg_id + ).pack() + ) From aadd2edf45b6657fe03d3dfb37573d4f37e7757b Mon Sep 17 00:00:00 2001 From: hteppl Date: Wed, 15 Jan 2025 08:01:16 +0300 Subject: [PATCH 17/23] Optimize and fix admin handlers logic --- handlers/admin/admin_backups.py | 20 ++- handlers/admin/admin_coupons.py | 21 +-- handlers/admin/admin_panel.py | 10 +- handlers/admin/admin_restart.py | 19 ++- handlers/admin/admin_sender.py | 19 ++- handlers/admin/admin_servers.py | 53 ++++--- handlers/admin/admin_stats.py | 20 ++- handlers/admin/admin_users.py | 262 ++++++++++++++++++-------------- 8 files changed, 231 insertions(+), 193 deletions(-) diff --git a/handlers/admin/admin_backups.py b/handlers/admin/admin_backups.py index d287659b..6d14fba5 100644 --- a/handlers/admin/admin_backups.py +++ b/handlers/admin/admin_backups.py @@ -3,7 +3,7 @@ from aiogram.types import CallbackQuery from backup import backup_database from filters.admin import IsAdminFilter -from keyboards.admin.panel_kb import AdminPanelCallback +from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb router = Router() @@ -12,11 +12,19 @@ router = Router() AdminPanelCallback.filter(F.action == "backups"), IsAdminFilter(), ) -async def handle_backup(callback_query: CallbackQuery): - await callback_query.message.answer( - text="💾 Инициализация резервного копирования базы данных..." +async def handle_backup( + callback_query: CallbackQuery +): + kb = build_admin_back_kb("management") + + await callback_query.message.edit_text( + text="💾 Инициализация резервного копирования базы данных...", + reply_markup=kb ) + await backup_database() - await callback_query.message.answer( - text="✅ Резервная копия успешно создана и отправлена администратору." + + await callback_query.message.edit_text( + text="✅ Резервная копия успешно создана и отправлена администратору.", + reply_markup=kb ) diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index ddcbf67b..b890b61e 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -23,15 +23,10 @@ class AdminCouponsState(StatesGroup): ) async def show_coupon_management_menu( callback_query: types.CallbackQuery, - state: FSMContext ): - await state.clear() - - kb = build_coupons_kb() - - await callback_query.message.answer( + await callback_query.message.edit_text( text="🛠 Меню управления купонами:", - reply_markup=kb + reply_markup=build_coupons_kb() ) @@ -49,7 +44,7 @@ async def handle_create_coupon( "Пример: 'COUPON1 50 5' 👈\n\n" ) - await callback_query.message.answer( + await callback_query.message.edit_text( text=text, reply_markup=build_admin_back_kb("coupons"), ) @@ -102,14 +97,14 @@ async def process_coupon_data( try: await create_coupon(coupon_code, coupon_amount, usage_limit, session) - result_message = ( + text = ( f"✅ Купон с кодом {coupon_code} успешно создан!\n" f"💰 Сумма: {coupon_amount} рублей \n" f"🔢 Лимит использования: {usage_limit} раз" ) await message.answer( - text=result_message, + text=text, reply_markup=kb ) await state.clear() @@ -130,8 +125,8 @@ async def show_coupon_list( coupons = await get_all_coupons(session) if not coupons: - await callback_query.message.answer( - text="❌ На данный момент нет доступных купонов. 🚫\nВы можете вернуться в меню управления. 🔙", + await callback_query.message.edit_text( + text="❌ На данный момент нет доступных купонов!", reply_markup=build_admin_back_kb("coupons"), ) return @@ -147,7 +142,7 @@ async def show_coupon_list( f"✅ Использовано: {coupon['usage_count']} раз\n\n" ) - await callback_query.message.answer( + await callback_query.message.edit_text( text=coupon_list, reply_markup=kb ) diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index d1ffbdf7..fe77f1d0 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -14,7 +14,11 @@ router = Router() IsAdminFilter() ) async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext): - await handle_admin_message(callback_query.message, state) + await state.clear() + await callback_query.message.edit_text( + text="🤖 Панель администратора", + reply_markup=build_panel_kb() + ) @router.callback_query( @@ -31,7 +35,7 @@ async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMC ) async def handle_admin_message(message: types.Message, state: FSMContext): await state.clear() - await message.message.answer( + await message.answer( text="🤖 Панель администратора", reply_markup=build_panel_kb() ) @@ -41,7 +45,7 @@ async def handle_admin_message(message: types.Message, state: FSMContext): AdminPanelCallback.filter(F.action == "management"), IsAdminFilter() ) -async def handle_restart(callback_query: CallbackQuery): +async def handle_management(callback_query: CallbackQuery): await callback_query.message.edit_text( text="🤖 Управление ботом", reply_markup=build_management_kb(), diff --git a/handlers/admin/admin_restart.py b/handlers/admin/admin_restart.py index 47cbc134..be39f694 100644 --- a/handlers/admin/admin_restart.py +++ b/handlers/admin/admin_restart.py @@ -14,10 +14,9 @@ router = Router() IsAdminFilter(), ) async def handle_restart(callback_query: CallbackQuery): - kb = build_restart_kb() - await callback_query.message.answer( - text="🤔 Вы уверены, что хотите перезапустить бота?", - reply_markup=kb, + await callback_query.message.edit_text( + text="🤔 Вы уверены, что хотите перезагрузить бота?", + reply_markup=build_restart_kb(), ) @@ -26,7 +25,7 @@ async def handle_restart(callback_query: CallbackQuery): IsAdminFilter(), ) async def confirm_restart_bot(callback_query: CallbackQuery): - kb = build_admin_back_kb("admin") + kb = build_admin_back_kb() try: subprocess.run( ["sudo", "systemctl", "restart", "bot.service"], @@ -34,17 +33,17 @@ async def confirm_restart_bot(callback_query: CallbackQuery): capture_output=True, text=True, ) - await callback_query.message.answer( - text="🔄 Бот успешно перезапущен.", + await callback_query.message.edit_text( + text="🔄 Бот успешно перезагружен!", reply_markup=kb ) except subprocess.CalledProcessError: - await callback_query.message.answer( - text="🔄 Бот успешно перезапущен.", + await callback_query.message.edit_text( + text="🔄 Бот успешно перезагружен!", reply_markup=kb ) except Exception as e: - await callback_query.message.answer( + await callback_query.message.edit_text( text=f"⚠️ Ошибка при перезагрузке бота: {e.stderr}", reply_markup=kb ) diff --git a/handlers/admin/admin_sender.py b/handlers/admin/admin_sender.py index 101aaf86..9eb91b55 100644 --- a/handlers/admin/admin_sender.py +++ b/handlers/admin/admin_sender.py @@ -26,14 +26,14 @@ class AdminSender(StatesGroup): async def handle_sender( callback_query: CallbackQuery ): - await callback_query.message.answer( + await callback_query.message.edit_text( text="✍️ Выберите группу пользователей для рассылки:", reply_markup=build_sender_kb(), ) @router.callback_query( - AdminSenderCallback, + AdminSenderCallback.filter(), IsAdminFilter(), ) async def handle_sender_callback( @@ -41,7 +41,7 @@ async def handle_sender_callback( callback_data: AdminSenderCallback, state: FSMContext ): - await callback_query.message.answer( + await callback_query.message.edit_text( text="✍️ Введите текст сообщения для рассылки:", reply_markup=build_admin_back_kb("sender"), ) @@ -93,11 +93,16 @@ async def process_message( except Exception: pass + text = ( + f"📤 Рассылка завершена!" + f"\n\n👥 Всего пользователей: {total_users}" + f"\n✅ Доставлено: {success_count}" + f"\n❌ Не доставлено: {total_users - success_count}" + ) + await message.answer( - f"📤 Рассылка завершена:\n" - f"👥 Всего пользователей: {total_users}\n" - f"✅ Доставлено: {success_count}\n" - f"❌ Не доставлено: {total_users - success_count}" + text=text, + reply_markup=build_admin_back_kb("stats") ) except Exception as e: logger.error(f"❗ Ошибка при подключении к базе данных: {e}") diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 0cab2ec4..1b12d60e 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -25,7 +25,7 @@ class AdminServersEditor(StatesGroup): @router.callback_query( - AdminPanelCallback.filter(F.action == "servers_editor"), + AdminPanelCallback.filter(F.action == "servers"), IsAdminFilter(), ) async def handle_servers_editor( @@ -41,7 +41,7 @@ async def handle_servers_editor( "⚠️ Важно: Кластеры удаляются автоматически, если удалить все серверы внутри них.\n\n" ) - await callback_query.message.answer( + await callback_query.message.edit_text( text=text, parse_mode="HTML", reply_markup=build_clusters_editor_kb(servers), @@ -62,7 +62,7 @@ async def handle_add_cluster( "Пример: cluster1 или us_east_1" ) - await callback_query.message.answer( + await callback_query.message.edit_text( text=text, parse_mode="HTML", ) @@ -93,10 +93,10 @@ async def handle_cluster_name_input( "Пример: server-frankfurt1, fra1" ) - await message.answer( + await message.edit_text( text=text, parse_mode="HTML", - reply_markup=build_admin_back_kb("servers_editor"), + reply_markup=build_admin_back_kb("servers"), ) await state.set_state(AdminServersEditor.waiting_for_server_name) @@ -135,10 +135,10 @@ async def handle_server_name_input( "URL должен быть без слэша на конце!\n" ) - await message.answer( + await message.edit_text( text=text, parse_mode="HTML", - reply_markup=build_admin_back_kb("servers_editor"), + reply_markup=build_admin_back_kb("servers"), ) await state.set_state(AdminServersEditor.waiting_for_api_url) @@ -174,7 +174,7 @@ async def handle_api_url_input( "Его можно увидеть в панели 3x-ui в информации о клиенте." ) - await message.answer( + await message.edit_text( text=text, parse_mode="HTML", reply_markup=build_cancel_kb(), @@ -210,10 +210,10 @@ async def handle_subscription_url_input( "Это номер подключения vless в вашей панели 3x-ui. Обычно это 1 при чистой настройке по гайду.\n\n" ) - await message.answer( + await message.edit_text( text=text, parse_mode="HTML", - reply_markup=build_admin_back_kb("servers_editor"), + reply_markup=build_admin_back_kb("servers"), ) await state.set_state(AdminServersEditor.waiting_for_inbound_id) @@ -254,9 +254,9 @@ async def handle_inbound_id_input( ) await conn.close() - await message.answer( + await message.edit_text( text=f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!", - reply_markup=build_admin_back_kb("servers_editor"), + reply_markup=build_admin_back_kb("servers"), ) await state.clear() @@ -275,7 +275,7 @@ async def handle_manage_cluster( servers = await get_servers_from_db() cluster_servers = servers.get(cluster_name, []) - await callback_query.message.answer( + await callback_query.message.edit_text( text=f"🔧 Управление серверами для кластера {cluster_name}", reply_markup=build_manage_cluster_kb(cluster_servers, cluster_name), ) @@ -295,7 +295,7 @@ async def handle_check_server_availability( cluster_servers = servers.get(cluster_name, []) if not cluster_servers: - await callback_query.answer( + await callback_query.message.answer( text=f"Кластер '{cluster_name}' не содержит серверов." ) return @@ -331,11 +331,9 @@ async def handle_check_server_availability( await in_progress_message.edit_text( text=text, - reply_markup=build_admin_back_kb("servers_editor") + reply_markup=build_admin_back_kb("servers") ) - await callback_query.answer() - @router.callback_query( AdminServerEditorCallback.filter(F.action == "servers_manage"), @@ -366,13 +364,15 @@ async def handle_manage_server( f"🔑 Inbound ID: {inbound_id}" ) - await callback_query.message.answer( + await callback_query.message.edit_text( text=text, parse_mode="HTML", reply_markup=build_manage_server_kb(server_name, cluster_name), ) else: - await callback_query.message.answer("❌ Сервер не найден.") + await callback_query.message.edit_text( + text="❌ Сервер не найден." + ) @router.callback_query( @@ -385,7 +385,7 @@ async def handle_delete_server( ): server_name = callback_data.data - await callback_query.message.answer( + await callback_query.message.edit_text( text=f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?", reply_markup=build_delete_server_kb(server_name), ) @@ -410,9 +410,9 @@ async def handle_confirm_delete_server( ) await conn.close() - await callback_query.message.answer( + await callback_query.message.edit_text( text=f"🗑️ Сервер {server_name} успешно удален.", - reply_markup=build_admin_back_kb("servers_editor") + reply_markup=build_admin_back_kb("servers") ) @@ -435,10 +435,10 @@ async def handle_add_server( "Пример: server-asia, server-europe" ) - await callback_query.message.answer( + await callback_query.message.edit_text( text=text, parse_mode="HTML", - reply_markup=build_admin_back_kb("servers_editor"), + reply_markup=build_admin_back_kb("servers"), ) await state.set_state(AdminServersEditor.waiting_for_server_name) @@ -470,9 +470,8 @@ async def handle_backup_cluster( f"🔔 Бэкапы отправлены в боты панелей." ) - await callback_query.message.answer( + await callback_query.message.edit_text( text=text, parse_mode="HTML", - reply_markup=build_admin_back_kb("servers_editor"), + reply_markup=build_admin_back_kb("servers"), ) - await callback_query.answer() diff --git a/handlers/admin/admin_stats.py b/handlers/admin/admin_stats.py index 113a6caa..c83456a8 100644 --- a/handlers/admin/admin_stats.py +++ b/handlers/admin/admin_stats.py @@ -93,7 +93,7 @@ async def export_users_csv(callback_query: CallbackQuery, session: Any): ) if not users: - await callback_query.message.answer( + await callback_query.message.edit_text( text="📭 Нет пользователей для экспорта.", reply_markup=kb ) @@ -109,16 +109,15 @@ async def export_users_csv(callback_query: CallbackQuery, session: Any): file = BufferedInputFile(file_name.getvalue(), filename="users_export.csv") await callback_query.message.answer_document( - file, - caption="📥 Экспорт пользователей в CSV", - reply_markup=kb + document=file, + caption="📥 Экспорт пользователей в CSV" ) file_name.close() except Exception as e: logger.error(f"Ошибка при экспорте пользователей в CSV: {e}") - await callback_query.message.answer( - text="❗ Произошла ошибка при экспорте пользователей.", + await callback_query.message.edit_text( + text=f"❗ Произошла ошибка при экспорте: {e}", reply_markup=kb ) @@ -148,7 +147,7 @@ async def export_payments_csv(callback_query: CallbackQuery, session: Any): ) if not payments: - await callback_query.message.answer( + await callback_query.message.edit_text( text="📭 Нет платежей для экспорта.", reply_markup=kb ) @@ -165,14 +164,13 @@ async def export_payments_csv(callback_query: CallbackQuery, session: Any): await callback_query.message.answer_document( document=file, - caption="📥 Экспорт платежей в CSV", - reply_markup=kb + caption="📥 Экспорт платежей в CSV" ) file_name.close() except Exception as e: logger.error(f"Ошибка при экспорте платежей в CSV: {e}") - await callback_query.message.answer( - text="❗ Произошла ошибка при экспорте платежей.", + await callback_query.message.edit_text( + text=f"❗ Произошла ошибка при экспорте: {e}", reply_markup=kb ) diff --git a/handlers/admin/admin_users.py b/handlers/admin/admin_users.py index be8b5361..a0ffb8a8 100644 --- a/handlers/admin/admin_users.py +++ b/handlers/admin/admin_users.py @@ -6,8 +6,8 @@ from aiogram import Bot, F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery -from config import TOTAL_GB +from config import TOTAL_GB from database import delete_user_data, get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry from filters.admin import IsAdminFilter from handlers.keys.key_utils import ( @@ -18,7 +18,7 @@ from handlers.keys.key_utils import ( from handlers.utils import sanitize_key_name from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb from keyboards.admin.users_kb import build_user_edit_kb, build_key_edit_kb, build_key_delete_kb, \ - build_user_delete_kb, AdminUserEditorCallback + build_user_delete_kb, AdminUserEditorCallback, build_editor_kb from logger import logger router = Router() @@ -35,7 +35,7 @@ class UserEditorState(StatesGroup): @router.callback_query( - AdminPanelCallback.filter(F.action == "users_search"), + AdminPanelCallback.filter(F.action == "search_user"), IsAdminFilter(), ) async def handle_users_search( @@ -44,19 +44,19 @@ async def handle_users_search( ): text = ( "🔍 Введите ID или Username пользователя для поиска:" - "\n— ID - числовой айди пользователя" - "\n— Username - юзернейм пользователя, начинающийся с @ или https://t.me/" + "\n\n🆔 ID - числовой айди" + "\n📝 Username - юзернейм пользователя" ) await state.set_state(UserEditorState.waiting_for_user_data) - await callback_query.message.answer( + await callback_query.message.edit_text( text=text, - reply_markup=build_admin_back_kb("users_editor") + reply_markup=build_admin_back_kb() ) @router.callback_query( - AdminPanelCallback.filter(F.action == "users_search_key"), + AdminPanelCallback.filter(F.action == "search_key"), IsAdminFilter(), ) async def handle_users_search_key( @@ -64,9 +64,9 @@ async def handle_users_search_key( state: FSMContext ): await state.set_state(UserEditorState.waiting_for_key_name) - await callback_query.message.answer( + await callback_query.message.edit_text( text="🔑 Введите имя ключа для поиска:", - reply_markup=build_admin_back_kb("users_editor") + reply_markup=build_admin_back_kb() ) @@ -79,11 +79,11 @@ async def handle_user_data_input( state: FSMContext, session: Any ): - kb = build_admin_back_kb("users_editor") + kb = build_admin_back_kb() if not message.text: await message.reply( - text="💢 Пожалуйста, отправьте текстовое сообщение.", + text="🚫 Пожалуйста, отправьте текстовое сообщение.", reply_markup=kb ) return @@ -101,11 +101,10 @@ async def handle_user_data_input( ) if not user: - await message.answer( - text="🔍 Пользователь с указанным username не найден. 🚫", + await message.reply( + text="🚫 Пользователь с указанным Username не найден!", reply_markup=kb, ) - await state.clear() return tg_id = user["tg_id"] @@ -113,23 +112,6 @@ async def handle_user_data_input( await process_user_search(message, state, session, tg_id) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_send_message"), - IsAdminFilter(), -) -async def handle_send_message( - callback_query: types.CallbackQuery, - callback_data: AdminUserEditorCallback, - state: FSMContext -): - tg_id = callback_data.data - await callback_query.message.answer( - "✉️ Введите текст сообщения, которое вы хотите отправить пользователю." - ) - await state.update_data(target_tg_id=tg_id) - await state.set_state(UserEditorState.waiting_for_message_text) - - @router.message( UserEditorState.waiting_for_message_text, IsAdminFilter() @@ -140,22 +122,46 @@ async def handle_message_text_input( bot: Bot ): data = await state.get_data() - target_tg_id = data.get("target_tg_id") - - if not target_tg_id: - await message.answer("🚫 Ошибка: ID пользователя не найден.") - await state.clear() - return + tg_id = data.get("tg_id") try: - await bot.send_message(chat_id=target_tg_id, text=message.text) - await message.answer("✅ Сообщение успешно отправлено.") + await bot.send_message( + chat_id=tg_id, + text=message.text + ) + await message.answer( + text="✅ Сообщение успешно отправлено.", + reply_markup=build_editor_kb(tg_id) + ) except Exception as e: - await message.answer(f"❌ Не удалось отправить сообщение: {e}") + await message.answer( + text=f"❌ Не удалось отправить сообщение: {e}", + reply_markup=build_editor_kb(tg_id) + ) await state.clear() +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_send_message"), + IsAdminFilter(), +) +async def handle_send_message( + callback_query: types.CallbackQuery, + callback_data: AdminUserEditorCallback, + state: FSMContext +): + tg_id = callback_data.tg_id + + await callback_query.message.edit_text( + text="✉️ Введите текст сообщения, которое вы хотите отправить пользователю:", + reply_markup=build_editor_kb(tg_id) + ) + + await state.update_data(tg_id=tg_id) + await state.set_state(UserEditorState.waiting_for_message_text) + + @router.callback_query( AdminUserEditorCallback.filter(F.action == "users_trial_restore"), IsAdminFilter(), @@ -165,11 +171,11 @@ async def handle_restore_trial( callback_data: AdminUserEditorCallback, session: Any ): - tg_id = int(callback_data.data) + tg_id = callback_data.tg_id await restore_trial(tg_id, session) - await callback_query.message.answer( - text="✅ Триал успешно восстановлен.", - reply_markup=build_admin_back_kb("admin") + await callback_query.message.edit_text( + text="✅ Триал успешно восстановлен!", + reply_markup=build_editor_kb(tg_id) ) @@ -182,11 +188,10 @@ async def process_balance_change( callback_data: AdminUserEditorCallback, state: FSMContext ): - tg_id = int(callback_data.data) - await state.update_data(tg_id=tg_id) - await callback_query.message.answer( + await state.update_data(tg_id=callback_data.tg_id) + await callback_query.message.edit_text( text="💸 Введите новую сумму баланса:", - reply_markup=build_admin_back_kb("users_editor") + reply_markup=build_editor_kb(callback_data.tg_id) ) await state.set_state(UserEditorState.waiting_for_new_balance) @@ -200,16 +205,17 @@ async def handle_new_balance_input( state: FSMContext, session: Any ): + user_data = await state.get_data() + tg_id = user_data.get("tg_id") + if not message.text.isdigit() or int(message.text) < 0: - await message.answer( + await message.edit_text( text="❌ Пожалуйста, введите корректную сумму для изменения баланса.", - reply_markup=build_admin_back_kb("users_editor"), + reply_markup=build_editor_kb(tg_id), ) return new_balance = int(message.text) - user_data = await state.get_data() - tg_id = user_data.get("tg_id") await session.execute( "UPDATE connections SET balance = $1 WHERE tg_id = $2", @@ -217,9 +223,9 @@ async def handle_new_balance_input( tg_id, ) - await message.answer( + await message.edit_text( text=f"✅ Баланс успешно изменен на {new_balance}.", - reply_markup=build_admin_back_kb("admin") + reply_markup=build_admin_back_kb() ) await state.clear() @@ -237,21 +243,21 @@ async def process_key_edit( key_details = await get_key_details(email, session) if not key_details: - await callback_query.message.answer( - text="🔍 Информация о ключе не найдена. 🚫", - reply_markup=build_admin_back_kb("users_editor"), + await callback_query.message.edit_text( + text="🚫 Информация о ключе не найдена.", + reply_markup=build_editor_kb(callback_data.tg_id), ) return - response_message = ( + text = ( f"🔑 Ключ: {key_details['key']}\n" f"⏰ Дата истечения: {key_details['expiry_date']}\n" f"💰 Баланс пользователя: {key_details['balance']}\n" f"🌐 Кластер: {key_details['server_name']}" ) - await callback_query.message.answer( - text=response_message, + await callback_query.message.edit_text( + text=text, reply_markup=build_key_edit_kb(key_details, email) ) @@ -269,22 +275,22 @@ async def handle_key_name_input( key_details = await get_key_details(key_name, session) if not key_details: - await message.answer( + await message.edit_text( text="🚫 Пользователь с указанным именем ключа не найден.", - reply_markup=build_admin_back_kb("users_editor") + reply_markup=build_admin_back_kb() ) await state.clear() return - response_message = ( + text = ( f"🔑 Ключ: {key_details['key']}\n" f"⏰ Дата истечения: {key_details['expiry_date']}\n" f"💰 Баланс пользователя: {key_details['balance']}\n" f"🌐 Сервер: {key_details['server_name']}" ) - await message.answer( - text=response_message, + await message.edit_text( + text=text, reply_markup=build_key_edit_kb(key_details, key_name) ) await state.clear() @@ -300,10 +306,10 @@ async def prompt_expiry_change( state: FSMContext ): email = callback_data.data - await callback_query.message.answer( + await callback_query.message.edit_text( text=f"⏳ Введите новое время истечения для ключа {email} в формате YYYY-MM-DD HH:MM:SS:" ) - await state.update_data(email=email) + await state.update_data(tg_id=callback_data.tg_id, email=email) await state.set_state(UserEditorState.waiting_for_expiry_time) @@ -319,36 +325,29 @@ async def handle_expiry_time_input( user_data = await state.get_data() email = user_data.get("email") - if not email: - await message.answer( - text="📧 Email не найден в состоянии. 🚫", - reply_markup=build_admin_back_kb("users_editor") - ) - await state.clear() - return - try: - expiry_time_str = message.text expiry_time = int( - datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").timestamp() * 1000 + datetime.strptime(message.text, "%Y-%m-%d %H:%M:%S").timestamp() * 1000 ) client_id = await get_client_id_by_email(email) + if client_id is None: - await message.answer( - text=f"🚫 Клиент с email {email} не найден. 🔍", - reply_markup=build_admin_back_kb("users_editor"), + await message.edit_text( + text=f"🚫 Клиент с Email {email} не найден. 🔍", + reply_markup=build_admin_back_kb(), ) await state.clear() return - record = await session.fetchrow( + server_id = await session.fetchrow( "SELECT server_id FROM keys WHERE client_id = $1", client_id ) - if not record: - await message.answer( + + if not server_id: + await message.edit_text( text="🚫 Клиент не найден в базе данных. 🔍", - reply_markup=build_admin_back_kb("users_editor"), + reply_markup=build_admin_back_kb(), ) await state.clear() return @@ -372,19 +371,19 @@ async def handle_expiry_time_input( await asyncio.gather(*tasks) await update_key_on_all_servers() - await update_key_expiry(client_id, expiry_time) response_message = f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах." - await message.answer( + await message.edit_text( text=response_message, - reply_markup=build_admin_back_kb("admin") + reply_markup=build_admin_back_kb() ) except ValueError: - await message.answer( + tg_id = user_data.get("tg_id") + await message.edit_text( text="❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.", - reply_markup=build_admin_back_kb("users_editor"), + reply_markup=build_editor_kb(tg_id), ) except Exception as e: logger.error(e) @@ -406,15 +405,15 @@ async def process_callback_delete_key( ) if client_id is None: - await callback_query.message.answer( - text="🔍 Ключ не найден. 🚫", - reply_markup=build_admin_back_kb("users_editor") + await callback_query.message.edit_text( + text="🚫 Ключ не найден!", + reply_markup=build_editor_kb(callback_data.tg_id) ) return - await callback_query.message.answer( - text="❓ Вы уверены, что хотите удалить ключ?", - reply_markup=build_key_delete_kb(client_id), + await callback_query.message.edit_text( + text="❓ Вы уверены, что хотите удалить ключ?", + reply_markup=build_key_delete_kb(callback_data.tg_id, client_id) ) @@ -432,7 +431,7 @@ async def process_callback_confirm_delete( "SELECT email FROM keys WHERE client_id = $1", client_id ) - kb = build_admin_back_kb("users_editor") + kb = build_editor_kb(callback_data.tg_id) if record: clusters = await get_servers_from_db() @@ -449,12 +448,12 @@ async def process_callback_confirm_delete( await delete_key_from_servers(record["email"], client_id) await delete_key_from_db(client_id, session) - await callback_query.message.answer( + await callback_query.message.edit_text( text="✅ Ключ успешно удален.", reply_markup=kb ) else: - await callback_query.message.answer( + await callback_query.message.edit_text( text="🚫 Ключ не найден или уже удален.", reply_markup=kb ) @@ -464,12 +463,27 @@ async def process_callback_confirm_delete( AdminUserEditorCallback.filter(F.action == "users_delete_user"), IsAdminFilter() ) +async def confirm_delete_user( + callback_query: types.CallbackQuery, + callback_data: AdminUserEditorCallback +): + tg_id = callback_data.tg_id + await callback_query.message.edit_text( + text=f"❗️ Вы уверены, что хотите удалить пользователя с ID {tg_id}?", + reply_markup=build_user_delete_kb(tg_id) + ) + + +@router.callback_query( + AdminUserEditorCallback.filter(F.action == "users_delete_user_confirm"), + IsAdminFilter() +) async def delete_user( callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any ): - tg_id = int(callback_data.data) + tg_id = callback_data.tg_id key_records = await session.fetch("SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id) async def delete_keys_from_servers(): @@ -487,33 +501,43 @@ async def delete_user( try: await delete_user_data(session, tg_id) - await callback_query.message.answer( + await callback_query.message.edit_text( text=f"🗑️ Пользователь с ID {tg_id} был удален.", - reply_markup=build_admin_back_kb("users_editor") + reply_markup=build_editor_kb(callback_data.tg_id) ) except Exception as e: logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}") - await callback_query.message.answer( + await callback_query.message.edit_text( text=f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова." ) @router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_delete_user_confirm"), + AdminUserEditorCallback.filter(F.action == "users_editor"), IsAdminFilter() ) -async def confirm_delete_user( +async def handle_users_editor( callback_query: types.CallbackQuery, - callback_data: AdminUserEditorCallback + callback_data: AdminUserEditorCallback, + state: FSMContext, + session: Any ): - tg_id = int(callback_data.data) - await callback_query.message.answer( - text=f"Вы уверены, что хотите удалить пользователя с ID {tg_id}?", - reply_markup=build_user_delete_kb(tg_id) + await process_user_search( + callback_query.message, + state, + session, + callback_data.tg_id, + callback_data.data == "edit" ) -async def process_user_search(message: types.Message, state: FSMContext, session: Any, tg_id: int) -> None: +async def process_user_search( + message: types.Message, + state: FSMContext, + session: Any, + tg_id: int, + edit: bool = False +) -> None: username = await session.fetchval( "SELECT username FROM users WHERE tg_id = $1", tg_id ) @@ -524,7 +548,7 @@ async def process_user_search(message: types.Message, state: FSMContext, session if balance is None: await message.answer( text="🚫 Пользователь с указанным ID не найден!", - reply_markup=build_admin_back_kb("users_editor"), + reply_markup=build_admin_back_kb(), ) return @@ -542,10 +566,16 @@ async def process_user_search(message: types.Message, state: FSMContext, session f"🔑 Ключи (для редактирования нажмите на ключ):" ) - await message.answer( - text=text, - reply_markup=build_user_edit_kb(tg_id, key_records) - ) + if edit: + await message.edit_text( + text=text, + reply_markup=build_user_edit_kb(tg_id, key_records) + ) + else: + await message.answer( + text=text, + reply_markup=build_user_edit_kb(tg_id, key_records) + ) await state.clear() From f176a11ff994d61a8c02f37023b2b8a338184eba Mon Sep 17 00:00:00 2001 From: hteppl Date: Wed, 15 Jan 2025 08:02:04 +0300 Subject: [PATCH 18/23] Remove legacy admin_user_editor from release --- handlers/admin/admin_user_editor.py | 770 ---------------------------- 1 file changed, 770 deletions(-) delete mode 100644 handlers/admin/admin_user_editor.py diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py deleted file mode 100644 index da18697e..00000000 --- a/handlers/admin/admin_user_editor.py +++ /dev/null @@ -1,770 +0,0 @@ -import asyncio -from datetime import datetime -from typing import Any - -from aiogram import Bot, F, Router, types -from aiogram.fsm.context import FSMContext -from aiogram.fsm.state import State, StatesGroup -from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup -from aiogram.utils.keyboard import InlineKeyboardBuilder -from config import TOTAL_GB - -from database import ( - delete_user_data, - get_client_id_by_email, - get_servers_from_db, - restore_trial, - update_key_expiry, -) -from filters.admin import IsAdminFilter -from handlers.keys.key_utils import ( - delete_key_from_cluster, - delete_key_from_db, - renew_key_in_cluster, -) -from handlers.utils import sanitize_key_name -from logger import logger - -router = Router() - - -class UserEditorState(StatesGroup): - waiting_for_tg_id = State() - waiting_for_username = State() - displaying_user_info = State() - waiting_for_new_balance = State() - waiting_for_key_name = State() - waiting_for_expiry_time = State() - waiting_for_message_text = State() - - -@router.callback_query(F.data == "search_by_tg_id", IsAdminFilter()) -async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext): - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer( - "🔍 Введите Telegram ID клиента:", reply_markup=builder.as_markup() - ) - await state.set_state(UserEditorState.waiting_for_tg_id) - - -@router.callback_query(F.data == "search_by_username", IsAdminFilter()) -async def prompt_username(callback_query: CallbackQuery, state: FSMContext): - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer( - "🔍 Введите Username клиента:", reply_markup=builder.as_markup() - ) - await state.set_state(UserEditorState.waiting_for_username) - - -@router.message(UserEditorState.waiting_for_username, IsAdminFilter()) -async def handle_username_input( - message: types.Message, state: FSMContext, session: Any -): - username = message.text.strip().lstrip("@").replace("https://t.me/", "") - user_record = await session.fetchrow( - "SELECT tg_id FROM users WHERE username = $1", username - ) - - if not user_record: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer( - "🔍 Пользователь с указанным username не найден. 🚫", - reply_markup=builder.as_markup(), - ) - await state.clear() - return - - tg_id = user_record["tg_id"] - username = await session.fetchval( - "SELECT username FROM users WHERE tg_id = $1", tg_id - ) - balance = await session.fetchval( - "SELECT balance FROM connections WHERE tg_id = $1", tg_id - ) - key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) - referral_count = await session.fetchval( - "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id - ) - - if balance is None: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer( - "🚫 Пользователь с указанным tg_id не найден. 🔍", - reply_markup=builder.as_markup(), - ) - await state.clear() - return - - builder = InlineKeyboardBuilder() - - for (email,) in key_records: - builder.row( - InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}") - ) - - builder.row( - InlineKeyboardButton( - text="📝 Изменить баланс", - callback_data=f"change_balance_{tg_id}", - ) - ) - - builder.row( - InlineKeyboardButton( - text="🔄 Восстановить пробник", - callback_data=f"restore_trial_{tg_id}", - ) - ) - builder.row( - InlineKeyboardButton( - text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}" - ) - ) - builder.row( - InlineKeyboardButton( - text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}" - ) - ) - builder.row( - InlineKeyboardButton( - text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}" - ) - ) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - - user_info = ( - f"📊 Информация о пользователе:\n\n" - f"🆔 ID пользователя: {tg_id}\n" - f"👤 Логин пользователя: @{username}\n" - f"💰 Баланс: {balance}\n" - f"👥 Количество рефералов: {referral_count}\n" - f"🔑 Ключи (для редактирования нажмите на ключ):" - ) - await message.answer(user_info, reply_markup=builder.as_markup()) - await state.set_state(UserEditorState.displaying_user_info) - - -@router.callback_query(F.data.startswith("send_message_")) -async def handle_send_message(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.data.split("_")[2] - await state.update_data(target_tg_id=tg_id) - await callback_query.message.answer( - "✉️ Введите текст сообщения, которое вы хотите отправить пользователю." - ) - await state.set_state(UserEditorState.waiting_for_message_text) - - -@router.message(UserEditorState.waiting_for_message_text, IsAdminFilter()) -async def process_send_message(message: types.Message, state: FSMContext, bot: Bot): - data = await state.get_data() - target_tg_id = data.get("target_tg_id") - - if not target_tg_id: - await message.answer("🚫 Ошибка: ID пользователя не найден.") - await state.clear() - return - - try: - await bot.send_message(chat_id=target_tg_id, text=message.text) - await message.answer("✅ Сообщение успешно отправлено.") - except Exception as e: - await message.answer(f"❌ Не удалось отправить сообщение: {e}") - - await state.clear() - - -@router.message(UserEditorState.waiting_for_tg_id, F.text.isdigit(), IsAdminFilter()) -async def handle_tg_id_input(message: types.Message, state: FSMContext, session: Any): - tg_id = int(message.text) - username = await session.fetchval( - "SELECT username FROM users WHERE tg_id = $1", tg_id - ) - balance = await session.fetchval( - "SELECT balance FROM connections WHERE tg_id = $1", tg_id - ) - key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) - referral_count = await session.fetchval( - "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id - ) - - if balance is None: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer( - "❌ Пользователь с указанным tg_id не найден. 🔍", - reply_markup=builder.as_markup(), - ) - await state.clear() - return - - builder = InlineKeyboardBuilder() - - for (email,) in key_records: - builder.row( - InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}") - ) - - builder.row( - InlineKeyboardButton( - text="📝 Изменить баланс", - callback_data=f"change_balance_{tg_id}", - ) - ) - builder.row( - InlineKeyboardButton( - text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}" - ) - ) - builder.row( - InlineKeyboardButton( - text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}" - ) - ) - builder.row( - InlineKeyboardButton( - text="🔄 Восстановить пробник", - callback_data=f"restore_trial_{tg_id}", - ) - ) - builder.row( - InlineKeyboardButton( - text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}" - ) - ) - - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - - user_info = ( - f"📊 Информация о пользователе:\n\n" - f"🆔 ID пользователя: {tg_id}\n" - f"👤 Логин пользователя: @{username}\n" - f"💰 Баланс: {balance}\n" - f"👥 Количество рефералов: {referral_count}\n" - f"🔑 Ключи (для редактирования нажмите на ключ):" - ) - await message.answer(user_info, reply_markup=builder.as_markup()) - await state.set_state(UserEditorState.displaying_user_info) - - -@router.callback_query(F.data.startswith("restore_trial_"), IsAdminFilter()) -async def handle_restore_trial(callback_query: types.CallbackQuery, session: Any): - tg_id = int(callback_query.data.split("_")[2]) - - await restore_trial(tg_id, session) - - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔙 Назад в меню администратора", callback_data="admin" - ) - ) - - await callback_query.message.answer( - "✅ Триал успешно восстановлен.", reply_markup=builder.as_markup() - ) - - -@router.callback_query(F.data.startswith("change_balance_"), IsAdminFilter()) -async def process_balance_change(callback_query: CallbackQuery, state: FSMContext): - tg_id = int(callback_query.data.split("_")[2]) - await state.update_data(tg_id=tg_id) - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer( - "💸 Введите новую сумму баланса:", reply_markup=builder.as_markup() - ) - await state.set_state(UserEditorState.waiting_for_new_balance) - - -@router.message(UserEditorState.waiting_for_new_balance, IsAdminFilter()) -async def handle_new_balance_input( - message: types.Message, state: FSMContext, session: Any -): - if not message.text.isdigit() or int(message.text) < 0: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer( - "❌ Пожалуйста, введите корректную сумму для изменения баланса.", - reply_markup=builder.as_markup(), - ) - return - - new_balance = int(message.text) - user_data = await state.get_data() - tg_id = user_data.get("tg_id") - - await session.execute( - "UPDATE connections SET balance = $1 WHERE tg_id = $2", - new_balance, - tg_id, - ) - - response_message = f"✅ Баланс успешно изменен на {new_balance}." - - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔙 Назад в меню администратора", - callback_data="admin", - ) - ) - await message.answer(response_message, reply_markup=builder.as_markup()) - await state.clear() - - -async def get_key_details(email, session): - record = await session.fetchrow( - """ - SELECT k.key, k.expiry_time, k.server_id, c.tg_id, c.balance - FROM keys k - JOIN connections c ON k.tg_id = c.tg_id - WHERE k.email = $1 - """, - email, - ) - - if not record: - return None - - servers = await get_servers_from_db() - - cluster_name = "Неизвестный кластер" - for cluster_name, cluster_servers in servers.items(): - if any( - server["inbound_id"] == record["server_id"] for server in cluster_servers - ): - cluster_name = cluster_name - break - - expiry_date = datetime.utcfromtimestamp(record["expiry_time"] / 1000) - current_date = datetime.utcnow() - time_left = expiry_date - current_date - - if time_left.total_seconds() <= 0: - days_left_message = "Ключ истек." - elif time_left.days > 0: - days_left_message = f"Осталось дней: {time_left.days}" - else: - hours_left = time_left.seconds // 3600 - days_left_message = f"Осталось часов: {hours_left}" - - return { - "key": record["key"], - "expiry_date": expiry_date.strftime("%d %B %Y года"), - "days_left_message": days_left_message, - "server_name": cluster_name, - "balance": record["balance"], - "tg_id": record["tg_id"], - } - - -@router.callback_query(F.data.startswith("edit_key_"), IsAdminFilter()) -async def process_key_edit(callback_query: CallbackQuery, session: Any): - email = callback_query.data.split("_", 2)[2] - key_details = await get_key_details(email, session) - - if not key_details: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer( - "🔍 Информация о ключе не найдена. 🚫", - reply_markup=builder.as_markup(), - ) - return - - response_message = ( - f"🔑 Ключ: {key_details['key']}\n" - f"⏰ Дата истечения: {key_details['expiry_date']}\n" - f"💰 Баланс пользователя: {key_details['balance']}\n" - f"🌐 Кластер: {key_details['server_name']}" - ) - - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="ℹ️ Получить информацию о юзере", - callback_data=f"user_info|{key_details['tg_id']}", - ) - ) - builder.row( - InlineKeyboardButton( - text="⏳ Изменить время истечения", - callback_data=f"change_expiry|{email}", - ) - ) - builder.row( - InlineKeyboardButton( - text="❌ Удалить ключ", - callback_data=f"delete_key_admin|{email}", - ) - ) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - - await callback_query.message.answer( - response_message, reply_markup=builder.as_markup() - ) - - -@router.callback_query(F.data == "search_by_key_name", IsAdminFilter()) -async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext): - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer( - "🔑 Введите имя ключа:", reply_markup=builder.as_markup() - ) - await state.set_state(UserEditorState.waiting_for_key_name) - - -@router.message(UserEditorState.waiting_for_key_name, IsAdminFilter()) -async def handle_key_name_input( - message: types.Message, state: FSMContext, session: Any -): - key_name = sanitize_key_name(message.text) - key_details = await get_key_details(key_name, session) - - if not key_details: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer( - "🚫 Пользователь с указанным именем ключа не найден.", - reply_markup=builder.as_markup(), - ) - await state.clear() - return - - response_message = ( - f"🔑 Ключ: {key_details['key']}\n" - f"⏰ Дата истечения: {key_details['expiry_date']}\n" - f"💰 Баланс пользователя: {key_details['balance']}\n" - f"🌐 Сервер: {key_details['server_name']}" - ) - - key_buttons = InlineKeyboardBuilder() - key_buttons.row( - InlineKeyboardButton( - text="ℹ️ Получить информацию о юзере", - callback_data=f"user_info|{key_details['tg_id']}", - ) - ) - key_buttons.row( - InlineKeyboardButton( - text="⏳ Изменить время истечения", - callback_data=f"change_expiry|{key_name}", - ) - ) - key_buttons.row( - InlineKeyboardButton( - text="❌ Удалить ключ", - callback_data=f"delete_key_admin|{key_name}", - ) - ) - key_buttons.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - - await message.answer(response_message, reply_markup=key_buttons.as_markup()) - await state.clear() - - -@router.callback_query(F.data.startswith("change_expiry|"), IsAdminFilter()) -async def prompt_expiry_change(callback_query: CallbackQuery, state: FSMContext): - email = callback_query.data.split("|")[1] - await callback_query.message.answer( - f"⏳ Введите новое время истечения для ключа {email} в формате YYYY-MM-DD HH:MM:SS:" - ) - await state.update_data(email=email) - await state.set_state(UserEditorState.waiting_for_expiry_time) - - -@router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter()) -async def handle_expiry_time_input( - message: types.Message, state: FSMContext, session: Any -): - user_data = await state.get_data() - email = user_data.get("email") - - if not email: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer( - "📧 Email не найден в состоянии. 🚫", reply_markup=builder.as_markup() - ) - await state.clear() - return - - try: - expiry_time_str = message.text - expiry_time = int( - datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").timestamp() * 1000 - ) - - client_id = await get_client_id_by_email(email) - if client_id is None: - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor") - ) - await message.answer( - f"🚫 Клиент с email {email} не найден. 🔍", - reply_markup=builder.as_markup(), - ) - await state.clear() - return - - record = await session.fetchrow( - "SELECT server_id FROM keys WHERE client_id = $1", client_id - ) - if not record: - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor") - ) - await message.answer( - "🚫 Клиент не найден в базе данных. 🔍", - reply_markup=builder.as_markup(), - ) - await state.clear() - return - - clusters = await get_servers_from_db() - - async def update_key_on_all_servers(): - tasks = [] - for cluster_name, cluster_servers in clusters.items(): - for server in cluster_servers: - tasks.append( - asyncio.create_task( - renew_key_in_cluster( - cluster_name, - email, - client_id, - expiry_time, - total_gb=TOTAL_GB, - ) - ) - ) - await asyncio.gather(*tasks) - - await update_key_on_all_servers() - - await update_key_expiry(client_id, expiry_time) - - response_message = f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах." - - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) - await message.answer(response_message, reply_markup=builder.as_markup()) - except ValueError: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer( - "❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.", - reply_markup=builder.as_markup(), - ) - except Exception as e: - logger.error(e) - await state.clear() - - -@router.callback_query(F.data.startswith("delete_key_admin|"), IsAdminFilter()) -async def process_callback_delete_key( - callback_query: types.CallbackQuery, session: Any -): - email = callback_query.data.split("|")[1] - client_id = await session.fetchval( - "SELECT client_id FROM keys WHERE email = $1", email - ) - - if client_id is None: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer( - "🔍 Ключ не найден. 🚫", reply_markup=builder.as_markup() - ) - return - - builder = InlineKeyboardBuilder() - builder.row( - types.InlineKeyboardButton( - text="✅ Да, удалить", - callback_data=f"confirm_delete_admin|{client_id}", - ) - ) - builder.row( - types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="user_editor") - ) - await callback_query.message.answer( - "❓ Вы уверены, что хотите удалить ключ?", - reply_markup=builder.as_markup(), - ) - - -@router.callback_query(F.data.startswith("confirm_delete_admin|"), IsAdminFilter()) -async def process_callback_confirm_delete( - callback_query: types.CallbackQuery, session: Any -): - client_id = callback_query.data.split("|")[1] - record = await session.fetchrow( - "SELECT email FROM keys WHERE client_id = $1", client_id - ) - - if record: - email = record["email"] - response_message = "✅ Ключ успешно удален." - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys")) - - clusters = await get_servers_from_db() - - async def delete_key_from_servers(email, client_id): - tasks = [] - for cluster_name, cluster_servers in clusters.items(): - for server in cluster_servers: - tasks.append( - delete_key_from_cluster(cluster_name, email, client_id) - ) - await asyncio.gather(*tasks) - - await delete_key_from_servers(email, client_id) - await delete_key_from_db(client_id, session) - - await callback_query.message.answer( - response_message, reply_markup=builder.as_markup() - ) - else: - response_message = "🚫 Ключ не найден или уже удален." - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys")) - await callback_query.message.answer( - response_message, reply_markup=builder.as_markup() - ) - - -@router.callback_query(F.data.startswith("user_info|"), IsAdminFilter()) -async def handle_user_info( - callback_query: types.CallbackQuery, state: FSMContext, session: Any -): - tg_id = int(callback_query.data.split("|")[1]) - username = await session.fetchval( - "SELECT username FROM users WHERE tg_id = $1", tg_id - ) - balance = await session.fetchval( - "SELECT balance FROM connections WHERE tg_id = $1", tg_id - ) - key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) - referral_count = await session.fetchval( - "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id - ) - - builder = InlineKeyboardBuilder() - - for (email,) in key_records: - builder.row( - InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}") - ) - - builder.row( - InlineKeyboardButton( - text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}" - ) - ) - builder.row( - InlineKeyboardButton( - text="🔄 Восстановить пробник", callback_data=f"restore_trial_{tg_id}" - ) - ) - builder.row( - InlineKeyboardButton( - text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}" - ) - ) - builder.row( - InlineKeyboardButton( - text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}" - ) - ) - builder.row( - InlineKeyboardButton( - text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}" - ) - ) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - - user_info = ( - f"📊 Информация о пользователе:\n\n" - f"🆔 ID пользователя: {tg_id}\n" - f"👤 Логин пользователя: @{username}\n" - f"💰 Баланс: {balance}\n" - f"👥 Количество рефералов: {referral_count}\n" - f"🔑 Ключи (для редактирования нажмите на ключ):" - ) - - await callback_query.message.answer(user_info, reply_markup=builder.as_markup()) - await state.set_state(UserEditorState.displaying_user_info) - - -@router.callback_query(F.data.startswith("confirm_delete_user_"), IsAdminFilter()) -async def confirm_delete_user( - callback_query: types.CallbackQuery, state: FSMContext, session: Any -): - tg_id = int(callback_query.data.split("_")[3]) - - confirmation_markup = InlineKeyboardMarkup( - row_width=2, - inline_keyboard=[ - [ - InlineKeyboardButton( - text="✅ Подтвердить", callback_data=f"delete_user_{tg_id}" - ) - ], - [InlineKeyboardButton(text="❌ Отменить", callback_data="user_editor")], - ], - ) - - await callback_query.message.answer( - f"Вы уверены, что хотите удалить пользователя с ID {tg_id}?", - reply_markup=confirmation_markup, - ) - - -@router.callback_query(F.data.startswith("delete_user_"), IsAdminFilter()) -async def delete_user(callback_query: types.CallbackQuery, session: Any): - tg_id = int(callback_query.data.split("_")[2]) - - key_records = await session.fetch( - "SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id - ) - - async def delete_keys_from_servers(): - try: - tasks = [] - for email, client_id in key_records: - servers = await get_servers_from_db() - for cluster_id, cluster in servers.items(): - tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) - await asyncio.gather(*tasks) - except Exception as e: - logger.error( - f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}" - ) - - await delete_keys_from_servers() - - try: - await delete_user_data(session, tg_id) - - back_button = InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor") - keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]]) - - await callback_query.message.answer( - f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=keyboard - ) - except Exception as e: - logger.error( - f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}" - ) - await callback_query.message.answer( - f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова." - ) From 450b8e17f0b7cb995d4ba5f13ff68f1d5761861d Mon Sep 17 00:00:00 2001 From: hteppl Date: Wed, 15 Jan 2025 08:59:54 +0300 Subject: [PATCH 19/23] Update delete middleware: pass all admin callbacks and states --- middlewares/delete.py | 78 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 16 deletions(-) diff --git a/middlewares/delete.py b/middlewares/delete.py index 846ed89a..570eb0aa 100644 --- a/middlewares/delete.py +++ b/middlewares/delete.py @@ -4,25 +4,71 @@ from typing import Any from aiogram import BaseMiddleware from aiogram.types import CallbackQuery, Message, TelegramObject +from keyboards.admin.coupons_kb import AdminCouponDeleteCallback +from keyboards.admin.panel_kb import AdminPanelCallback +from keyboards.admin.sender_kb import AdminSenderCallback +from keyboards.admin.servers_kb import AdminServerEditorCallback +from keyboards.admin.users_kb import AdminUserEditorCallback + +pass_callbacks = [ + AdminPanelCallback, + AdminCouponDeleteCallback, + AdminSenderCallback, + AdminServerEditorCallback, + AdminUserEditorCallback, +] + +pass_states = [ + 'UserEditorState', + 'AdminServersEditor', + 'AdminSender', + 'AdminCouponsState' +] + class DeleteMessageMiddleware(BaseMiddleware): async def __call__( - self, - handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], - event: TelegramObject, - data: dict[str, Any], + self, + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], + event: TelegramObject, + data: dict[str, Any], ) -> Any: - if isinstance(event, (Message, CallbackQuery)): - if isinstance(event, Message): - if not event.text or not event.text.startswith("/start"): - try: - await event.bot.delete_message( - event.chat.id, event.message_id - 1 - ) - except Exception: - pass - await event.delete() - elif isinstance(event, CallbackQuery): - await event.answer() + if isinstance(event, Message): + if ( + not event.text or + not event.text.startswith("/start") or + not self._check_user_state(event) + ): + try: + await event.bot.delete_message( + event.chat.id, event.message_id - 1 + ) + except Exception: + pass + await event.delete() + + if isinstance(event, CallbackQuery): + await event.answer() + + if not await self._check_callbacks(event): await event.message.delete() + return await handler(event, data) + + @staticmethod + async def _check_user_state(event: CallbackQuery) -> bool: + from bot import dp + context = dp.fsm.get_context( + bot=event.bot, + user_id=event.from_user.id, + chat_id=event.chat.id + ) + state = await context.get_state() + return state.split(":")[0] in pass_states + + @staticmethod + async def _check_callbacks(event: CallbackQuery) -> bool: + for callback in pass_callbacks: + if await callback.filter()(event): + return True + return False From e3bb7f6663bc0eb27ad13f2c569af0aabbc07925 Mon Sep 17 00:00:00 2001 From: hteppl Date: Wed, 15 Jan 2025 09:01:10 +0300 Subject: [PATCH 20/23] Remove direct key search, use common user searcher anytime --- handlers/admin/admin_users.py | 99 +++++++++++++++-------------------- keyboards/admin/users_kb.py | 7 --- 2 files changed, 43 insertions(+), 63 deletions(-) diff --git a/handlers/admin/admin_users.py b/handlers/admin/admin_users.py index a0ffb8a8..ad25db21 100644 --- a/handlers/admin/admin_users.py +++ b/handlers/admin/admin_users.py @@ -2,7 +2,7 @@ import asyncio from datetime import datetime from typing import Any -from aiogram import Bot, F, Router, types +from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery @@ -113,33 +113,25 @@ async def handle_user_data_input( @router.message( - UserEditorState.waiting_for_message_text, + UserEditorState.waiting_for_key_name, IsAdminFilter() ) -async def handle_message_text_input( +async def handle_key_name_input( message: types.Message, state: FSMContext, - bot: Bot + session: Any ): - data = await state.get_data() - tg_id = data.get("tg_id") + key_name = sanitize_key_name(message.text) + key_details = await get_key_details(key_name, session) - try: - await bot.send_message( - chat_id=tg_id, - text=message.text - ) - await message.answer( - text="✅ Сообщение успешно отправлено.", - reply_markup=build_editor_kb(tg_id) - ) - except Exception as e: - await message.answer( - text=f"❌ Не удалось отправить сообщение: {e}", - reply_markup=build_editor_kb(tg_id) + if not key_details: + await message.edit_text( + text="🚫 Пользователь с указанным именем ключа не найден.", + reply_markup=build_admin_back_kb() ) + return - await state.clear() + await process_user_search(message, state, session, key_details["tg_id"]) @router.callback_query( @@ -162,6 +154,35 @@ async def handle_send_message( await state.set_state(UserEditorState.waiting_for_message_text) +@router.message( + UserEditorState.waiting_for_message_text, + IsAdminFilter() +) +async def handle_message_text_input( + message: types.Message, + state: FSMContext +): + data = await state.get_data() + tg_id = data.get("tg_id") + + try: + await message.bot.send_message( + chat_id=tg_id, + text=message.text + ) + await message.answer( + text="✅ Сообщение успешно отправлено.", + reply_markup=build_editor_kb(tg_id) + ) + except Exception as e: + await message.answer( + text=f"❌ Не удалось отправить сообщение: {e}", + reply_markup=build_editor_kb(tg_id) + ) + + await state.clear() + + @router.callback_query( AdminUserEditorCallback.filter(F.action == "users_trial_restore"), IsAdminFilter(), @@ -262,40 +283,6 @@ async def process_key_edit( ) -@router.message( - UserEditorState.waiting_for_key_name, - IsAdminFilter() -) -async def handle_key_name_input( - message: types.Message, - state: FSMContext, - session: Any -): - key_name = sanitize_key_name(message.text) - key_details = await get_key_details(key_name, session) - - if not key_details: - await message.edit_text( - text="🚫 Пользователь с указанным именем ключа не найден.", - reply_markup=build_admin_back_kb() - ) - await state.clear() - return - - text = ( - f"🔑 Ключ: {key_details['key']}\n" - f"⏰ Дата истечения: {key_details['expiry_date']}\n" - f"💰 Баланс пользователя: {key_details['balance']}\n" - f"🌐 Сервер: {key_details['server_name']}" - ) - - await message.edit_text( - text=text, - reply_markup=build_key_edit_kb(key_details, key_name) - ) - await state.clear() - - @router.callback_query( AdminUserEditorCallback.filter(F.action == "users_change_expiry"), IsAdminFilter() @@ -538,6 +525,8 @@ async def process_user_search( tg_id: int, edit: bool = False ) -> None: + await state.clear() + username = await session.fetchval( "SELECT username FROM users WHERE tg_id = $1", tg_id ) @@ -577,8 +566,6 @@ async def process_user_search( reply_markup=build_user_edit_kb(tg_id, key_records) ) - await state.clear() - async def get_key_details(email, session): record = await session.fetchrow( diff --git a/keyboards/admin/users_kb.py b/keyboards/admin/users_kb.py index 0bd6cf86..22c080e1 100644 --- a/keyboards/admin/users_kb.py +++ b/keyboards/admin/users_kb.py @@ -71,13 +71,6 @@ def build_user_delete_kb(tg_id: int): def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="ℹ️ Получить информацию о юзере", - callback_data=AdminUserEditorCallback( - action="users_info", - tg_id=key_details["tg_id"] - ).pack() - ) builder.button( text="⏳ Изменить время истечения", callback_data=AdminUserEditorCallback( From a7d0cca66ccf63538c0e31a6e61dc9867e626f42 Mon Sep 17 00:00:00 2001 From: hteppl Date: Wed, 15 Jan 2025 09:01:56 +0300 Subject: [PATCH 21/23] Do not import bot directly in admin sender --- handlers/admin/admin_sender.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/handlers/admin/admin_sender.py b/handlers/admin/admin_sender.py index 9eb91b55..7f691d5d 100644 --- a/handlers/admin/admin_sender.py +++ b/handlers/admin/admin_sender.py @@ -6,7 +6,6 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery -from bot import bot from filters.admin import IsAdminFilter from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb from keyboards.admin.sender_kb import build_sender_kb, AdminSenderCallback @@ -88,7 +87,10 @@ async def process_message( for record in tg_ids: tg_id = record["tg_id"] try: - await bot.send_message(chat_id=tg_id, text=text_message) + await message.bot.send_message( + chat_id=tg_id, + text=text_message + ) success_count += 1 except Exception: pass From 34b1927c22dc375d090728ceff118e9ea47a5e3d Mon Sep 17 00:00:00 2001 From: hteppl Date: Wed, 15 Jan 2025 14:07:49 +0300 Subject: [PATCH 22/23] Some missing logic and fixups --- handlers/admin/admin_users.py | 11 ++++++++++- keyboards/admin/users_kb.py | 11 +++++++++++ middlewares/delete.py | 8 ++++---- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/handlers/admin/admin_users.py b/handlers/admin/admin_users.py index ad25db21..3667a312 100644 --- a/handlers/admin/admin_users.py +++ b/handlers/admin/admin_users.py @@ -121,13 +121,22 @@ async def handle_key_name_input( state: FSMContext, session: Any ): + kb = build_admin_back_kb() + + if not message.text: + await message.reply( + text="🚫 Пожалуйста, отправьте текстовое сообщение.", + reply_markup=kb + ) + return + key_name = sanitize_key_name(message.text) key_details = await get_key_details(key_name, session) if not key_details: await message.edit_text( text="🚫 Пользователь с указанным именем ключа не найден.", - reply_markup=build_admin_back_kb() + reply_markup=kb ) return diff --git a/keyboards/admin/users_kb.py b/keyboards/admin/users_kb.py index 22c080e1..66f958f6 100644 --- a/keyboards/admin/users_kb.py +++ b/keyboards/admin/users_kb.py @@ -32,6 +32,14 @@ def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup: tg_id=tg_id ).pack() ) + builder.button( + text="✉️ Сообщение", + callback_data=AdminUserEditorCallback( + action="users_send_message", + tg_id=tg_id + ).pack() + ) + builder.button( text="🔄 Восстановить триал", callback_data=AdminUserEditorCallback( @@ -46,6 +54,9 @@ def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup: tg_id=tg_id ).pack() ) + builder.row( + build_editor_btn("🔄 Обновить", tg_id, edit=True) + ) builder.row( build_admin_back_btn() ) diff --git a/middlewares/delete.py b/middlewares/delete.py index 570eb0aa..52d4d7ae 100644 --- a/middlewares/delete.py +++ b/middlewares/delete.py @@ -19,10 +19,10 @@ pass_callbacks = [ ] pass_states = [ - 'UserEditorState', - 'AdminServersEditor', - 'AdminSender', - 'AdminCouponsState' + "UserEditorState", + "AdminServersEditor", + "AdminSender", + "AdminCouponsState" ] From 7154374fdc305d2bbdaebc2c25a1a91df48fa10a Mon Sep 17 00:00:00 2001 From: hteppl Date: Wed, 15 Jan 2025 14:29:33 +0300 Subject: [PATCH 23/23] Update stats, fix some text input flows to working with delete middleware --- handlers/admin/admin_coupons.py | 2 +- handlers/admin/admin_stats.py | 30 ++++++++++++++++++++++++------ handlers/admin/admin_users.py | 14 +++++++------- middlewares/delete.py | 23 ++--------------------- 4 files changed, 34 insertions(+), 35 deletions(-) diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index b890b61e..357b9946 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -168,7 +168,7 @@ async def handle_delete_coupon( if result: await show_coupon_list(callback_query, session) else: - await callback_query.message.answer( + await callback_query.message.edit_text( text=f"❌ Купон с кодом {coupon_code} не найден.", ) await show_coupon_list(callback_query, session) diff --git a/handlers/admin/admin_stats.py b/handlers/admin/admin_stats.py index c83456a8..e5e4a869 100644 --- a/handlers/admin/admin_stats.py +++ b/handlers/admin/admin_stats.py @@ -19,7 +19,7 @@ router = Router() ) async def user_stats_menu(callback_query: CallbackQuery, session: Any): try: - total_users = await session.fetchval("SELECT COUNT(*) FROM connections") + total_users = await session.fetchval("SELECT COUNT(*) FROM users") total_keys = await session.fetchval("SELECT COUNT(*) FROM keys") total_referrals = await session.fetchval("SELECT COUNT(*) FROM referrals") @@ -36,6 +36,20 @@ async def user_stats_menu(callback_query: CallbackQuery, session: Any): "SELECT COALESCE(SUM(amount), 0) FROM payments" ) + registrations_today = await session.fetchval( + "SELECT COUNT(*) FROM users WHERE created_at >= CURRENT_DATE" + ) + registrations_week = await session.fetchval( + "SELECT COUNT(*) FROM users WHERE created_at >= date_trunc('week', CURRENT_DATE)" + ) + registrations_month = await session.fetchval( + "SELECT COUNT(*) FROM users WHERE created_at >= date_trunc('month', CURRENT_DATE)" + ) + + users_updated_today = await session.fetchval( + "SELECT COUNT(*) FROM users WHERE updated_at >= CURRENT_DATE" + ) + active_keys = await session.fetchval( "SELECT COUNT(*) FROM keys WHERE expiry_time > $1", int(datetime.utcnow().timestamp() * 1000), @@ -45,8 +59,14 @@ async def user_stats_menu(callback_query: CallbackQuery, session: Any): stats_message = ( f"📊 Подробная статистика проекта:\n\n" f"👥 Пользователи:\n" - f" 🌐 Зарегистрировано: {total_users}\n" - f" 🤝 Привлеченных рефералов: {total_referrals}\n\n" + f" 📅 За день: {registrations_today}\n" + f" 📆 За неделю: {registrations_week}\n" + f" 📆 За месяц: {registrations_month}\n" + f" 🌐 За все время: {total_users}\n\n" + f"🌟 Активные пользователи:\n" + f" 🌟 Активных сегодня: {users_updated_today}\n\n" + f"👥 Рефералы:\n" + f" 🤝 Всего привлечено: {total_referrals}\n\n" f"🔑 Ключи:\n" f" 🌈 Всего сгенерировано: {total_keys}\n" f" ✅ Действующих: {active_keys}\n" @@ -58,11 +78,9 @@ async def user_stats_menu(callback_query: CallbackQuery, session: Any): f" 🏦 За все время: {total_payments_all_time} ₽\n" ) - kb = build_stats_kb() - await callback_query.message.edit_text( text=stats_message, - reply_markup=kb + reply_markup=build_stats_kb() ) except Exception as e: logger.error(f"Error in user_stats_menu: {e}") diff --git a/handlers/admin/admin_users.py b/handlers/admin/admin_users.py index 3667a312..4f48a5ac 100644 --- a/handlers/admin/admin_users.py +++ b/handlers/admin/admin_users.py @@ -82,7 +82,7 @@ async def handle_user_data_input( kb = build_admin_back_kb() if not message.text: - await message.reply( + await message.answer( text="🚫 Пожалуйста, отправьте текстовое сообщение.", reply_markup=kb ) @@ -101,7 +101,7 @@ async def handle_user_data_input( ) if not user: - await message.reply( + await message.answer( text="🚫 Пользователь с указанным Username не найден!", reply_markup=kb, ) @@ -124,7 +124,7 @@ async def handle_key_name_input( kb = build_admin_back_kb() if not message.text: - await message.reply( + await message.answer( text="🚫 Пожалуйста, отправьте текстовое сообщение.", reply_markup=kb ) @@ -134,7 +134,7 @@ async def handle_key_name_input( key_details = await get_key_details(key_name, session) if not key_details: - await message.edit_text( + await message.answer( text="🚫 Пользователь с указанным именем ключа не найден.", reply_markup=kb ) @@ -239,7 +239,7 @@ async def handle_new_balance_input( tg_id = user_data.get("tg_id") if not message.text.isdigit() or int(message.text) < 0: - await message.edit_text( + await message.answer( text="❌ Пожалуйста, введите корректную сумму для изменения баланса.", reply_markup=build_editor_kb(tg_id), ) @@ -253,8 +253,8 @@ async def handle_new_balance_input( tg_id, ) - await message.edit_text( - text=f"✅ Баланс успешно изменен на {new_balance}.", + await message.answer( + text=f"✅ Баланс успешно изменен на {new_balance}", reply_markup=build_admin_back_kb() ) await state.clear() diff --git a/middlewares/delete.py b/middlewares/delete.py index 52d4d7ae..b3ccdd37 100644 --- a/middlewares/delete.py +++ b/middlewares/delete.py @@ -18,13 +18,6 @@ pass_callbacks = [ AdminUserEditorCallback, ] -pass_states = [ - "UserEditorState", - "AdminServersEditor", - "AdminSender", - "AdminCouponsState" -] - class DeleteMessageMiddleware(BaseMiddleware): async def __call__( @@ -35,9 +28,8 @@ class DeleteMessageMiddleware(BaseMiddleware): ) -> Any: if isinstance(event, Message): if ( - not event.text or - not event.text.startswith("/start") or - not self._check_user_state(event) + not event.text + or not event.text.startswith("/start") ): try: await event.bot.delete_message( @@ -55,17 +47,6 @@ class DeleteMessageMiddleware(BaseMiddleware): return await handler(event, data) - @staticmethod - async def _check_user_state(event: CallbackQuery) -> bool: - from bot import dp - context = dp.fsm.get_context( - bot=event.bot, - user_id=event.from_user.id, - chat_id=event.chat.id - ) - state = await context.get_state() - return state.split(":")[0] in pass_states - @staticmethod async def _check_callbacks(event: CallbackQuery) -> bool: for callback in pass_callbacks: