From c678a9bfcec09b66ef71ec74d8f93cb4cbff2b93 Mon Sep 17 00:00:00 2001 From: Vladless Date: Sun, 27 Oct 2024 22:45:16 +0300 Subject: [PATCH] =?UTF-8?q?/admin.=20GUI=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80?= =?UTF-8?q?=D1=84=D0=B5=D0=B9=D1=81=20=D0=B2=20=D0=B1=D0=BE=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 4 +- handlers/admin/admin.py | 7 - handlers/admin/admin_panel.py | 77 +++++++ handlers/admin/user_editor.py | 368 ++++++++++++++++++++++++++++++++++ handlers/commands.py | 9 +- handlers/notifications.py | 16 +- 6 files changed, 462 insertions(+), 19 deletions(-) create mode 100644 handlers/admin/admin_panel.py create mode 100644 handlers/admin/user_editor.py diff --git a/bot.py b/bot.py index b7c90090..cae09814 100644 --- a/bot.py +++ b/bot.py @@ -8,12 +8,14 @@ storage = MemoryStorage() dp = Dispatcher(bot=bot, storage=storage) router = Router() -from handlers.admin import admin +from handlers.admin import admin, admin_panel, user_editor from handlers.keys import key_management, keys from handlers import (notifications, pay, profile, start, commands) dp.include_router(admin.router) +dp.include_router(admin_panel.router) +dp.include_router(user_editor.router) dp.include_router(commands.router) dp.include_router(start.router) dp.include_router(profile.router) diff --git a/handlers/admin/admin.py b/handlers/admin/admin.py index b8e4b7d8..cc3f6485 100644 --- a/handlers/admin/admin.py +++ b/handlers/admin/admin.py @@ -45,16 +45,13 @@ async def cmd_update_key_expiry(message: types.Message): _, email, expiry_time_str = parts expiry_time = int(datetime.strptime(expiry_time_str, '%Y-%m-%d %H:%M:%S').timestamp() * 1000) - # Получаем client_id по email client_id = await get_client_id_by_email(email) if client_id is None: await message.reply(f"Клиент с email {email} не найден.") return - # Обновляем время истечения ключа в базе данных await update_key_expiry(client_id, expiry_time) - # Подключение для получения server_id и tg_id conn = await asyncpg.connect(DATABASE_URL) try: record = await conn.fetchrow('SELECT server_id FROM keys WHERE client_id = $1', client_id) @@ -65,16 +62,12 @@ async def cmd_update_key_expiry(message: types.Message): server_id = record['server_id'] tg_id = await get_tg_id_by_client_id(client_id) - # Авторизация на панели session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) - # Вывод обновленных данных перед отправкой на панель print(f"Попытка обновить панель для server_id: {server_id}, tg_id: {tg_id}, client_id: {client_id}, email: {email}, expiryTime: {expiry_time}") - # Обновляем время истечения ключа на панели success = await extend_client_key_admin(session, server_id, tg_id, client_id, email, expiry_time) - # Проверка успеха и ответ пользователю print(f"Статус обновления панели: {'Успешно' if success else 'Не удалось'}") if success: await message.reply(f"Время истечения ключа для клиента {client_id} ({email}) обновлено и синхронизировано с панелью.") diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py new file mode 100644 index 00000000..7c0bf379 --- /dev/null +++ b/handlers/admin/admin_panel.py @@ -0,0 +1,77 @@ +from aiogram import Router, types +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery +from aiogram.filters import Command +from aiogram.fsm.state import StatesGroup, State +from config import ADMIN_ID, DATABASE_URL +import asyncpg +from datetime import datetime +from bot import bot + +router = Router() + +class UserEditorState(StatesGroup): + waiting_for_tg_id = State() + displaying_user_info = State() + +@router.message(Command('admin')) +async def handle_admin_command(message: types.Message): + if message.from_user.id != ADMIN_ID: + await message.reply("У вас нет доступа к этой команде.") + return + + keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="Статистика пользователей", callback_data="user_stats")], + [InlineKeyboardButton(text="Редактор пользователей", callback_data="user_editor")] + ]) + await message.reply("Панель администратора", reply_markup=keyboard) + +@router.callback_query(lambda c: c.data == "user_stats") +async def user_stats_menu(callback_query: CallbackQuery): + conn = await asyncpg.connect(DATABASE_URL) + try: + total_users = await conn.fetchval("SELECT COUNT(*) FROM connections") + total_keys = await conn.fetchval("SELECT COUNT(*) FROM keys") + total_referrals = await conn.fetchval("SELECT COUNT(*) FROM referrals") + + active_keys = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE expiry_time > $1", int(datetime.utcnow().timestamp() * 1000)) + expired_keys = total_keys - active_keys + + stats_message = ( + f"🔹 Общая статистика пользователей:\n" + f"• Всего пользователей: {total_users}\n" + f"• Всего ключей: {total_keys}\n" + f"• Всего рефералов: {total_referrals}\n" + f"• Активные ключи: {active_keys}\n" + f"• Истекшие ключи: {expired_keys}" + ) + + back_button = InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu") + keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [back_button] + ]) + + await callback_query.message.edit_text(stats_message, reply_markup=keyboard, parse_mode="HTML") + finally: + await conn.close() + + await callback_query.answer() + +@router.callback_query(lambda c: c.data == "user_editor") +async def user_editor_menu(callback_query: CallbackQuery): + keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="Поиск по имени ключа", callback_data="search_by_key_name")], + [InlineKeyboardButton(text="Поиск по tg_id", callback_data="search_by_tg_id")], + [InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu")] # Back button + ]) + await callback_query.message.edit_text("Выберите метод поиска:", reply_markup=keyboard) + +@router.callback_query(lambda c: c.data == "back_to_admin_menu") +async def back_to_admin_menu(callback_query: CallbackQuery): + keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="Статистика пользователей", callback_data="user_stats")], + [InlineKeyboardButton(text="Редактор пользователей", callback_data="user_editor")] + ]) + await callback_query.message.edit_text("Панель администратора", reply_markup=keyboard) + +async def handle_error(tg_id, callback_query, message): + await bot.edit_message_text(message, chat_id=tg_id, message_id=callback_query.message.message_id) diff --git a/handlers/admin/user_editor.py b/handlers/admin/user_editor.py new file mode 100644 index 00000000..cdd475d0 --- /dev/null +++ b/handlers/admin/user_editor.py @@ -0,0 +1,368 @@ +from aiogram import Router, types, F +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import StatesGroup, State +from config import DATABASE_URL, SERVERS +import asyncpg +from datetime import datetime +from bot import bot +from database import update_key_expiry, get_client_id_by_email, get_tg_id_by_client_id +from config import DATABASE_URL, ADMIN_PASSWORD, ADMIN_USERNAME +from datetime import datetime +import asyncpg +from auth import login_with_credentials +from client import extend_client_key_admin +from handlers.admin.admin_panel import back_to_admin_menu +from client import delete_client + +router = Router() + +class UserEditorState(StatesGroup): + waiting_for_tg_id = State() + displaying_user_info = State() + waiting_for_new_balance = State() + waiting_for_key_name = State() + waiting_for_expiry_time = State() + +@router.callback_query(lambda c: c.data == "search_by_tg_id") +async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext): + await callback_query.message.edit_text("Введите tg_id клиента:") + await state.set_state(UserEditorState.waiting_for_tg_id) + +@router.message(UserEditorState.waiting_for_tg_id, F.text.isdigit()) +async def handle_tg_id_input(message: types.Message, state: FSMContext): + tg_id = int(message.text) + + conn = await asyncpg.connect(DATABASE_URL) + try: + balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) + key_records = await conn.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) + referral_count = await conn.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) + + if balance is None: + await message.reply("Пользователь с указанным tg_id не найден.") + await state.clear() + return + + key_buttons = [ + [InlineKeyboardButton(text=email, callback_data=f"edit_key_{email}")] + for email, in key_records + ] + keyboard = InlineKeyboardMarkup(inline_keyboard=[ + *key_buttons, + [InlineKeyboardButton(text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}")], + [InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor")] + ]) + + user_info = ( + f"Информация о пользователе:\n" + f"Баланс: {balance}\n" + f"Количество рефералов: {referral_count}\n" + f"Ключи (для редактирования нажмите на ключ):" + ) + await message.reply(user_info, reply_markup=keyboard, parse_mode="HTML") + await state.set_state(UserEditorState.displaying_user_info) + + finally: + await conn.close() + +@router.callback_query(lambda c: c.data.startswith('change_balance_')) +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) + + await callback_query.message.edit_text("Введите новую сумму баланса:") + await callback_query.answer() + await state.set_state(UserEditorState.waiting_for_new_balance) + +@router.message(UserEditorState.waiting_for_new_balance) +async def handle_new_balance_input(message: types.Message, state: FSMContext): + if not message.text.isdigit() or int(message.text) < 0: + await message.reply("Пожалуйста, введите корректную сумму для изменения баланса.") + return + + new_balance = int(message.text) + user_data = await state.get_data() + tg_id = user_data.get('tg_id') + + conn = await asyncpg.connect(DATABASE_URL) + try: + await conn.execute("UPDATE connections SET balance = $1 WHERE tg_id = $2", new_balance, tg_id) + + response_message = f"Баланс успешно изменен на {new_balance}." + + back_button = InlineKeyboardButton(text="Назад в меню админа", callback_data="back_to_user_editor") + keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]]) + + await message.reply(response_message, reply_markup=keyboard, parse_mode="HTML") + + finally: + await conn.close() + + await state.clear() + + +@router.callback_query(lambda c: c.data.startswith('edit_key_')) +async def process_key_edit(callback_query: CallbackQuery): + email = callback_query.data.split('_', 2)[2] + + try: + conn = await asyncpg.connect(DATABASE_URL) + try: + record = await conn.fetchrow(''' + SELECT k.key, k.expiry_time, k.server_id + FROM keys k + WHERE k.email = $1 + ''', email) + + if record: + key = record['key'] + expiry_time = record['expiry_time'] + server_id = record['server_id'] + server_name = SERVERS.get(server_id, {}).get('name', 'Неизвестный сервер') + + expiry_date = datetime.utcfromtimestamp(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}" + + formatted_expiry_date = expiry_date.strftime('%d %B %Y года') + + response_message = ( + f"Ключ:
{key}
\n" + f"Дата истечения: {formatted_expiry_date}\n" + f"{days_left_message}\n" + f"Сервер: {server_name}" + ) + + change_expiry_button = types.InlineKeyboardButton(text='⏳ Изменить время истечения', callback_data=f'change_expiry|{email}') + delete_button = types.InlineKeyboardButton(text='❌ Удалить ключ', callback_data=f'delete_key_admin|{email}') + + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [change_expiry_button, delete_button], + [InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor")] # Кнопка "Назад" + ] + ) + + await callback_query.message.edit_text(response_message, reply_markup=keyboard, parse_mode="HTML") + else: + await callback_query.message.edit_text("Информация о ключе не найдена.", parse_mode="HTML") + + finally: + await conn.close() + + except Exception as e: + await handle_error(callback_query.from_user.id, callback_query, f"Ошибка при получении информации о ключе: {e}") + + await callback_query.answer() + +@router.callback_query(lambda c: c.data == "search_by_key_name") +async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext): + await callback_query.message.edit_text("Введите имя ключа:") + await state.set_state(UserEditorState.waiting_for_key_name) + +@router.message(UserEditorState.waiting_for_key_name) +async def handle_key_name_input(message: types.Message, state: FSMContext): + key_name = message.text + + conn = await asyncpg.connect(DATABASE_URL) + try: + user_records = await conn.fetch(''' + SELECT c.tg_id, c.balance, k.email, k.key, k.expiry_time, k.server_id + FROM connections c + JOIN keys k ON c.tg_id = k.tg_id + WHERE k.email = $1 + ''', key_name) + + if not user_records: + await message.reply("Пользователь с указанным именем ключа не найден.") + await state.clear() + return + + response_messages = [] + key_buttons = [] + + for record in user_records: + tg_id = record['tg_id'] + balance = record['balance'] + email = record['email'] + key = record['key'] + expiry_time = record['expiry_time'] + server_id = record['server_id'] + server_name = SERVERS.get(server_id, {}).get('name', 'Неизвестный сервер') + + expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime('%d %B %Y') + + response_messages.append( + f"Ключ:
{key}
\n" + f"Дата истечения: {expiry_date}\n" + f"Баланс пользователя: {balance}\n" + f"Сервер: {server_name}" + ) + + change_expiry_button = InlineKeyboardButton(text='⏳ Изменить время истечения', callback_data=f'change_expiry|{email}') + delete_button = InlineKeyboardButton(text='❌ Удалить ключ', callback_data=f'delete_key_admin|{email}') + + key_buttons.append([change_expiry_button, delete_button]) + + key_buttons.append([InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor")]) + + keyboard = InlineKeyboardMarkup(inline_keyboard=key_buttons) + + await message.reply("\n".join(response_messages), reply_markup=keyboard, parse_mode="HTML") + + finally: + await conn.close() + + await state.clear() + +@router.callback_query(lambda c: c.data.startswith('change_expiry|')) +async def prompt_expiry_change(callback_query: CallbackQuery, state: FSMContext): + email = callback_query.data.split('|')[1] + await callback_query.message.edit_text( + f"Введите новое время истечения для ключа {email} в формате YYYY-MM-DD HH:MM:SS:", + parse_mode="HTML" +) + await state.update_data(email=email) + await state.set_state(UserEditorState.waiting_for_expiry_time) + +@router.message(UserEditorState.waiting_for_expiry_time) +async def handle_expiry_time_input(message: types.Message, state: FSMContext): + user_data = await state.get_data() + email = user_data.get('email') + + if not email: + await message.reply("Email не найден в состоянии.") + 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.reply(f"Клиент с email {email} не найден.") + await state.clear() + return + + await update_key_expiry(client_id, expiry_time) + + conn = await asyncpg.connect(DATABASE_URL) + try: + record = await conn.fetchrow('SELECT server_id FROM keys WHERE client_id = $1', client_id) + if not record: + await message.reply("Клиент не найден в базе данных.") + await state.clear() + return + + server_id = record['server_id'] + tg_id = await get_tg_id_by_client_id(client_id) + + session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + + print(f"Попытка обновить панель для server_id: {server_id}, tg_id: {tg_id}, client_id: {client_id}, email: {email}, expiryTime: {expiry_time}") + + success = await extend_client_key_admin(session, server_id, tg_id, client_id, email, expiry_time) + + print(f"Статус обновления панели: {'Успешно' if success else 'Не удалось'}") + if success: + response_message = ( + f"Время истечения ключа для клиента {client_id} ({email}) успешно обновлено и синхронизировано с панелью." + ) + else: + response_message = ( + f"Время истечения ключа для клиента {client_id} ({email}) обновлено, но не удалось синхронизировать с панелью." + ) + + back_button = InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor") + keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]]) + + await message.reply(response_message, reply_markup=keyboard, parse_mode="HTML") + + finally: + await conn.close() + except ValueError: + await message.reply("Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.") + except Exception as e: + await message.reply(f"Произошла ошибка: {e}") + + await state.clear() + +@router.callback_query(lambda c: c.data.startswith('delete_key_admin|')) +async def process_callback_delete_key(callback_query: types.CallbackQuery): + tg_id = callback_query.from_user.id + email = callback_query.data.split('|')[1] + + conn = await asyncpg.connect(DATABASE_URL) + try: + client_id = await conn.fetchval('SELECT client_id FROM keys WHERE email = $1', email) + + if client_id is None: + await bot.edit_message_text("Ключ не найден.", chat_id=tg_id, message_id=callback_query.message.message_id) + return + + confirmation_keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text='✅ Да, удалить', callback_data=f'confirm_delete_admin|{client_id}')], + [types.InlineKeyboardButton(text='❌ Нет, отменить', callback_data='view_keys')] + ]) + + await bot.edit_message_text("Вы уверены, что хотите удалить ключ?", chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=confirmation_keyboard, parse_mode="HTML") + finally: + await conn.close() + + await callback_query.answer() + +@router.callback_query(lambda c: c.data.startswith('confirm_delete_admin|')) +async def process_callback_confirm_delete(callback_query: types.CallbackQuery): + tg_id = callback_query.from_user.id + client_id = callback_query.data.split('|')[1] + + try: + conn = await asyncpg.connect(DATABASE_URL) + try: + record = await conn.fetchrow('SELECT email, server_id FROM keys WHERE client_id = $1', client_id) + + if record: + email = record['email'] + server_id = record['server_id'] + session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + success = await delete_client(session, server_id, client_id) + + if success: + await conn.execute('DELETE FROM keys WHERE client_id = $1', client_id) + response_message = "Ключ был успешно удален." + else: + response_message = "Ошибка при удалении клиента через API." + + else: + response_message = "Ключ не найден или уже удален." + + back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys') + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) + + await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard) + + finally: + await conn.close() + + except Exception as e: + await bot.edit_message_text(f"Ошибка при удалении ключа: {e}", chat_id=tg_id, message_id=callback_query.message.message_id) + + await callback_query.answer() + +@router.callback_query(lambda c: c.data == "back_to_user_editor") +async def back_to_user_editor(callback_query: CallbackQuery): + await back_to_admin_menu(callback_query) + +async def handle_error(tg_id, callback_query, message): + await bot.edit_message_text(message, chat_id=tg_id, message_id=callback_query.message.message_id, parse_mode="HTML") + diff --git a/handlers/commands.py b/handlers/commands.py index 8473d65e..770d5147 100644 --- a/handlers/commands.py +++ b/handlers/commands.py @@ -47,6 +47,11 @@ async def handle_menu(message: types.Message, state: FSMContext): @router.message(Command('send_trial')) async def handle_send_trial_command(message: types.Message, state: FSMContext): + # Проверка на администратора + if message.from_user.id != ADMIN_ID: + await message.reply("У вас нет доступа к этой команде.") + return + try: conn = await asyncpg.connect(DATABASE_URL) try: @@ -57,9 +62,7 @@ async def handle_send_trial_command(message: types.Message, state: FSMContext): if records: for record in records: tg_id = record['tg_id'] - trial_message = ( - TRIAL - ) + trial_message = TRIAL try: await bot.send_message(chat_id=tg_id, text=trial_message) except Exception as e: diff --git a/handlers/notifications.py b/handlers/notifications.py index 8c907dce..acee1668 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -46,10 +46,10 @@ async def notify_expiring_keys(bot: Bot): async def is_bot_blocked(bot: Bot, chat_id: int) -> bool: try: member = await bot.get_chat_member(chat_id, bot.id) - return member.status == 'left' # Если бот не в чате, он заблокирован + return member.status == 'left' except Exception as e: logger.error(f"Ошибка при проверке статуса бота у пользователя {chat_id}: {e}") - return False # Если произошла ошибка, предполагаем, что бот не заблокирован + return False async def notify_10h_keys(bot: Bot, conn: asyncpg.Connection, current_time: float, threshold_time_10h: float): records = await conn.fetch(''' @@ -73,12 +73,12 @@ async def notify_10h_keys(bot: Bot, conn: asyncpg.Connection, current_time: floa logger.info(f"Уведомление отправлено пользователю {tg_id}.") except Exception as e: logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}") - continue # Пропустить этого пользователя и продолжить + continue await conn.execute('UPDATE keys SET notified = TRUE WHERE client_id = $1', record['client_id']) logger.info(f"Обновлено поле notified для клиента {record['client_id']}.") - await asyncio.sleep(1) # Задержка между уведомлениями + await asyncio.sleep(1) async def notify_24h_keys(bot: Bot, conn: asyncpg.Connection, current_time: float, threshold_time_24h: float): logger.info("Проверка истекших ключей...") @@ -109,17 +109,17 @@ async def notify_24h_keys(bot: Bot, conn: asyncpg.Connection, current_time: floa logger.info(f"Уведомление за 24 часа отправлено пользователю {tg_id}.") except Exception as e: logger.error(f"Ошибка при отправке уведомления за 24 часа пользователю {tg_id}: {e}") - continue # Пропустить этого пользователя и продолжить + continue await conn.execute('UPDATE keys SET notified_24h = TRUE WHERE client_id = $1', record['client_id']) logger.info(f"Обновлено поле notified_24h для клиента {record['client_id']}.") - await asyncio.sleep(1) # Задержка между уведомлениями + await asyncio.sleep(1) async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: float): logger.info("Проверка истекших ключей...") - current_time = int(current_time) # Убедитесь, что время в миллисекундах + current_time = int(current_time) expiring_keys = await conn.fetch(''' SELECT tg_id, client_id, expiry_time, server_id, email FROM keys WHERE expiry_time <= $1 @@ -174,4 +174,4 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: except Exception as e: logger.error(f"Ошибка при отправке уведомления о неудачном удалении ключа пользователю {tg_id}: {e}") - await asyncio.sleep(1) # Задержка между обработками ключей + await asyncio.sleep(1)