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)