From ba53e9e199a1edecc602523a65175911fa09f33f Mon Sep 17 00:00:00 2001 From: hteppl Date: Thu, 9 Jan 2025 15:43:22 +0300 Subject: [PATCH] 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()