- Added `ca-certificates` installation to Dockerfile for improved network security. - Updated health check command in Dockerfile to include better timeout handling. - Refactored `run_helper.py` to implement proper signal handling and logging during shutdown. - Transitioned database operations to an asynchronous model in `async_db.py`, improving performance and responsiveness. - Updated database schema to support new foreign key relationships and optimized indexing for better query performance. - Enhanced various bot handlers to utilize async database methods, improving overall efficiency and user experience. - Removed obsolete database and fix scripts to streamline the project structure.
363 lines
14 KiB
Python
363 lines
14 KiB
Python
from aiogram import Router, types, F
|
||
from aiogram.filters import Command, StateFilter, MagicData
|
||
from aiogram.fsm.context import FSMContext
|
||
|
||
from helper_bot.filters.main import ChatTypeFilter
|
||
from helper_bot.keyboards.keyboards import (
|
||
get_reply_keyboard_admin,
|
||
create_keyboard_with_pagination,
|
||
create_keyboard_for_ban_days,
|
||
create_keyboard_for_approve_ban,
|
||
create_keyboard_for_ban_reason
|
||
)
|
||
from helper_bot.handlers.admin.dependencies import AdminAccessMiddleware
|
||
from helper_bot.handlers.admin.services import AdminService
|
||
from helper_bot.handlers.admin.exceptions import (
|
||
UserAlreadyBannedError,
|
||
InvalidInputError
|
||
)
|
||
from helper_bot.handlers.admin.utils import (
|
||
return_to_admin_menu,
|
||
handle_admin_error,
|
||
format_user_info,
|
||
format_ban_confirmation,
|
||
escape_html
|
||
)
|
||
from logs.custom_logger import logger
|
||
|
||
# Создаем роутер с middleware для проверки доступа
|
||
admin_router = Router()
|
||
admin_router.message.middleware(AdminAccessMiddleware())
|
||
|
||
|
||
# ============================================================================
|
||
# ХЕНДЛЕРЫ МЕНЮ
|
||
# ============================================================================
|
||
|
||
@admin_router.message(
|
||
ChatTypeFilter(chat_type=["private"]),
|
||
Command('admin')
|
||
)
|
||
async def admin_panel(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
**kwargs
|
||
):
|
||
"""Главное меню администратора"""
|
||
try:
|
||
await state.set_state("ADMIN")
|
||
logger.info(f"Запуск админ панели для пользователя: {message.from_user.id}")
|
||
markup = get_reply_keyboard_admin()
|
||
await message.answer("Добро пожаловать в админку. Выбери что хочешь:", reply_markup=markup)
|
||
except Exception as e:
|
||
await handle_admin_error(message, e, state, "admin_panel")
|
||
|
||
|
||
@admin_router.message(
|
||
ChatTypeFilter(chat_type=["private"]),
|
||
StateFilter("ADMIN"),
|
||
F.text == 'Бан (Список)'
|
||
)
|
||
async def get_last_users(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
bot_db: MagicData("bot_db")
|
||
):
|
||
"""Получение списка последних пользователей"""
|
||
try:
|
||
logger.info(f"Получение списка последних пользователей. Пользователь: {message.from_user.full_name}")
|
||
admin_service = AdminService(bot_db)
|
||
users = await admin_service.get_last_users()
|
||
|
||
# Преобразуем в формат для клавиатуры (кортежи как ожидает create_keyboard_with_pagination)
|
||
users_data = [
|
||
(user.full_name, user.user_id)
|
||
for user in users
|
||
]
|
||
|
||
keyboard = create_keyboard_with_pagination(1, len(users_data), users_data, 'ban')
|
||
await message.answer(
|
||
text="Список пользователей которые последними обращались к боту",
|
||
reply_markup=keyboard
|
||
)
|
||
except Exception as e:
|
||
await handle_admin_error(message, e, state, "get_last_users")
|
||
|
||
|
||
@admin_router.message(
|
||
ChatTypeFilter(chat_type=["private"]),
|
||
StateFilter("ADMIN"),
|
||
F.text == 'Разбан (список)'
|
||
)
|
||
async def get_banned_users(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
bot_db: MagicData("bot_db")
|
||
):
|
||
"""Получение списка заблокированных пользователей"""
|
||
try:
|
||
logger.info(f"Получение списка заблокированных пользователей. Пользователь: {message.from_user.full_name}")
|
||
admin_service = AdminService(bot_db)
|
||
message_text, buttons_list = await admin_service.get_banned_users_for_display(0)
|
||
|
||
if buttons_list:
|
||
keyboard = create_keyboard_with_pagination(1, len(buttons_list), buttons_list, 'unlock')
|
||
await message.answer(text=message_text, reply_markup=keyboard)
|
||
else:
|
||
await message.answer(text="В списке заблокированных пользователей никого нет")
|
||
except Exception as e:
|
||
await handle_admin_error(message, e, state, "get_banned_users")
|
||
|
||
|
||
# ============================================================================
|
||
# ХЕНДЛЕРЫ ПРОЦЕССА БАНА
|
||
# ============================================================================
|
||
|
||
@admin_router.message(
|
||
ChatTypeFilter(chat_type=["private"]),
|
||
StateFilter("ADMIN"),
|
||
F.text.in_(['Бан по нику', 'Бан по ID'])
|
||
)
|
||
async def start_ban_process(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
**kwargs
|
||
):
|
||
"""Начало процесса блокировки пользователя"""
|
||
try:
|
||
ban_type = "username" if message.text == 'Бан по нику' else "id"
|
||
await state.update_data(ban_type=ban_type)
|
||
|
||
prompt_text = "Пришли мне username блокируемого пользователя" if ban_type == "username" else "Пришли мне ID блокируемого пользователя"
|
||
await message.answer(prompt_text)
|
||
await state.set_state('AWAIT_BAN_TARGET')
|
||
except Exception as e:
|
||
await handle_admin_error(message, e, state, "start_ban_process")
|
||
|
||
|
||
@admin_router.message(
|
||
ChatTypeFilter(chat_type=["private"]),
|
||
StateFilter("AWAIT_BAN_TARGET")
|
||
)
|
||
async def process_ban_target(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
bot_db: MagicData("bot_db")
|
||
):
|
||
"""Обработка введенного username/ID для блокировки"""
|
||
try:
|
||
user_data = await state.get_data()
|
||
ban_type = user_data.get('ban_type')
|
||
admin_service = AdminService(bot_db)
|
||
|
||
|
||
# Определяем пользователя
|
||
if ban_type == "username":
|
||
user = await admin_service.get_user_by_username(message.text)
|
||
if not user:
|
||
await message.answer(f"Пользователь с username '{escape_html(message.text)}' не найден.")
|
||
await return_to_admin_menu(message, state)
|
||
return
|
||
else: # ban_type == "id"
|
||
try:
|
||
user_id = await admin_service.validate_user_input(message.text)
|
||
user = await admin_service.get_user_by_id(user_id)
|
||
if not user:
|
||
await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
|
||
await return_to_admin_menu(message, state)
|
||
return
|
||
except InvalidInputError as e:
|
||
await message.answer(str(e))
|
||
await return_to_admin_menu(message, state)
|
||
return
|
||
|
||
# Сохраняем данные пользователя
|
||
await state.update_data(
|
||
target_user_id=user.user_id,
|
||
target_username=user.username,
|
||
target_full_name=user.full_name
|
||
)
|
||
|
||
# Показываем информацию о пользователе и запрашиваем причину
|
||
user_info = format_user_info(user.user_id, user.username, user.full_name)
|
||
markup = create_keyboard_for_ban_reason()
|
||
await message.answer(
|
||
text=f"{user_info}\n\nВыбери причину бана из списка или напиши ее в чат",
|
||
reply_markup=markup
|
||
)
|
||
await state.set_state('AWAIT_BAN_DETAILS')
|
||
|
||
except Exception as e:
|
||
await handle_admin_error(message, e, state, "process_ban_target")
|
||
|
||
|
||
@admin_router.message(
|
||
ChatTypeFilter(chat_type=["private"]),
|
||
StateFilter("AWAIT_BAN_DETAILS")
|
||
)
|
||
async def process_ban_reason(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
**kwargs
|
||
):
|
||
"""Обработка причины блокировки"""
|
||
try:
|
||
await state.update_data(ban_reason=message.text)
|
||
markup = create_keyboard_for_ban_days()
|
||
safe_reason = escape_html(message.text)
|
||
await message.answer(
|
||
f"Выбрана причина: {safe_reason}. Выбери срок бана в днях или напиши его в чат",
|
||
reply_markup=markup
|
||
)
|
||
await state.set_state('AWAIT_BAN_DURATION')
|
||
except Exception as e:
|
||
await handle_admin_error(message, e, state, "process_ban_reason")
|
||
|
||
|
||
@admin_router.message(
|
||
ChatTypeFilter(chat_type=["private"]),
|
||
StateFilter("AWAIT_BAN_DURATION")
|
||
)
|
||
async def process_ban_duration(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
**kwargs
|
||
):
|
||
"""Обработка срока блокировки"""
|
||
try:
|
||
user_data = await state.get_data()
|
||
|
||
# Определяем срок блокировки
|
||
if message.text == 'Навсегда':
|
||
ban_days = None
|
||
else:
|
||
try:
|
||
ban_days = int(message.text)
|
||
if ban_days <= 0:
|
||
await message.answer("Срок блокировки должен быть положительным числом.")
|
||
return
|
||
except ValueError:
|
||
await message.answer("Пожалуйста, введите корректное число дней или выберите 'Навсегда'.")
|
||
return
|
||
|
||
await state.update_data(ban_days=ban_days)
|
||
|
||
# Показываем подтверждение
|
||
confirmation_text = format_ban_confirmation(
|
||
user_data['target_user_id'],
|
||
user_data['ban_reason'],
|
||
ban_days
|
||
)
|
||
markup = create_keyboard_for_approve_ban()
|
||
await message.answer(confirmation_text, reply_markup=markup)
|
||
await state.set_state('BAN_CONFIRMATION')
|
||
|
||
except Exception as e:
|
||
await handle_admin_error(message, e, state, "process_ban_duration")
|
||
|
||
|
||
@admin_router.message(
|
||
ChatTypeFilter(chat_type=["private"]),
|
||
StateFilter("BAN_CONFIRMATION"),
|
||
F.text == 'Подтвердить'
|
||
)
|
||
async def confirm_ban(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
bot_db: MagicData("bot_db"),
|
||
**kwargs
|
||
):
|
||
"""Подтверждение блокировки пользователя"""
|
||
try:
|
||
user_data = await state.get_data()
|
||
admin_service = AdminService(bot_db)
|
||
|
||
|
||
# Выполняем блокировку
|
||
await admin_service.ban_user(
|
||
user_id=user_data['target_user_id'],
|
||
username=user_data['target_username'],
|
||
reason=user_data['ban_reason'],
|
||
ban_days=user_data['ban_days']
|
||
)
|
||
|
||
safe_username = escape_html(user_data['target_username'])
|
||
await message.reply(f"Пользователь {safe_username} успешно заблокирован.")
|
||
await return_to_admin_menu(message, state)
|
||
|
||
except UserAlreadyBannedError as e:
|
||
await message.reply(str(e))
|
||
await return_to_admin_menu(message, state)
|
||
except Exception as e:
|
||
await handle_admin_error(message, e, state, "confirm_ban")
|
||
|
||
|
||
# ============================================================================
|
||
# ХЕНДЛЕРЫ ОТМЕНЫ И НАВИГАЦИИ
|
||
# ============================================================================
|
||
|
||
@admin_router.message(
|
||
ChatTypeFilter(chat_type=["private"]),
|
||
StateFilter("AWAIT_BAN_TARGET", "AWAIT_BAN_DETAILS", "AWAIT_BAN_DURATION", "BAN_CONFIRMATION"),
|
||
F.text == 'Отменить'
|
||
)
|
||
async def cancel_ban_process(
|
||
message: types.Message,
|
||
state: FSMContext,
|
||
**kwargs
|
||
):
|
||
"""Отмена процесса блокировки"""
|
||
try:
|
||
current_state = await state.get_state()
|
||
logger.info(f"Отмена процедуры блокировки из состояния: {current_state}")
|
||
await return_to_admin_menu(message, state)
|
||
except Exception as e:
|
||
await handle_admin_error(message, e, state, "cancel_ban_process")
|
||
|
||
|
||
@admin_router.message(Command("test_metrics"))
|
||
async def test_metrics_handler(
|
||
message: types.Message,
|
||
bot_db: MagicData("bot_db"),
|
||
**kwargs
|
||
):
|
||
"""Тестовый хендлер для проверки метрик"""
|
||
from helper_bot.utils.metrics import metrics
|
||
|
||
try:
|
||
# Принудительно записываем тестовые метрики
|
||
metrics.record_command("test_metrics", "admin_handler", "admin", "success")
|
||
metrics.record_message("text", "private", "admin_handler")
|
||
metrics.record_error("TestError", "admin_handler", "test_metrics_handler")
|
||
|
||
# Проверяем активных пользователей
|
||
if hasattr(bot_db, 'connect') and hasattr(bot_db, 'cursor'):
|
||
# Используем UNIX timestamp для сравнения с date_changed
|
||
import time
|
||
current_timestamp = int(time.time())
|
||
one_day_ago = current_timestamp - (24 * 60 * 60) # 24 часа назад
|
||
|
||
active_users_query = """
|
||
SELECT COUNT(DISTINCT user_id) as active_users
|
||
FROM our_users
|
||
WHERE date_changed > ?
|
||
"""
|
||
try:
|
||
await bot_db.connect()
|
||
await bot_db.cursor.execute(active_users_query, (one_day_ago,))
|
||
result = await bot_db.cursor.fetchone()
|
||
active_users = result[0] if result else 0
|
||
finally:
|
||
await bot_db.close()
|
||
else:
|
||
active_users = "N/A"
|
||
|
||
await message.answer(
|
||
f"✅ Тестовые метрики записаны\n"
|
||
f"📊 Активных пользователей: {active_users}\n"
|
||
f"🔧 Проверьте Prometheus метрики"
|
||
)
|
||
|
||
except Exception as e:
|
||
await message.answer(f"❌ Ошибка тестирования метрик: {e}")
|