Merge remote-tracking branch 'origin/master' into dev-13

This commit is contained in:
2026-02-02 00:12:09 +03:00
105 changed files with 8845 additions and 8665 deletions

View File

@@ -1,37 +1,27 @@
from .admin_handlers import admin_router
from .dependencies import AdminAccessMiddleware, BotDB, Settings
from .exceptions import (
AdminAccessDeniedError,
AdminError,
InvalidInputError,
UserAlreadyBannedError,
UserNotFoundError,
)
from .exceptions import (AdminAccessDeniedError, AdminError, InvalidInputError,
UserAlreadyBannedError, UserNotFoundError)
from .services import AdminService, BannedUser, User
from .utils import (
escape_html,
format_ban_confirmation,
format_user_info,
handle_admin_error,
return_to_admin_menu,
)
from .utils import (escape_html, format_ban_confirmation, format_user_info,
handle_admin_error, return_to_admin_menu)
__all__ = [
"admin_router",
"AdminAccessMiddleware",
"BotDB",
"Settings",
"AdminService",
"User",
"BannedUser",
"AdminError",
"AdminAccessDeniedError",
"UserNotFoundError",
"InvalidInputError",
"UserAlreadyBannedError",
"return_to_admin_menu",
"handle_admin_error",
"format_user_info",
"format_ban_confirmation",
"escape_html",
]
'admin_router',
'AdminAccessMiddleware',
'BotDB',
'Settings',
'AdminService',
'User',
'BannedUser',
'AdminError',
'AdminAccessDeniedError',
'UserNotFoundError',
'InvalidInputError',
'UserAlreadyBannedError',
'return_to_admin_menu',
'handle_admin_error',
'format_user_info',
'format_ban_confirmation',
'escape_html'
]

View File

@@ -1,29 +1,22 @@
from aiogram import F, Router, types
from aiogram.filters import Command, MagicData, StateFilter
from aiogram.fsm.context import FSMContext
from helper_bot.filters.main import ChatTypeFilter
from helper_bot.handlers.admin.dependencies import AdminAccessMiddleware
from helper_bot.handlers.admin.exceptions import (
InvalidInputError,
UserAlreadyBannedError,
)
from helper_bot.handlers.admin.exceptions import (InvalidInputError,
UserAlreadyBannedError)
from helper_bot.handlers.admin.services import AdminService
from helper_bot.handlers.admin.utils import (
escape_html,
format_ban_confirmation,
format_user_info,
handle_admin_error,
return_to_admin_menu,
)
from helper_bot.keyboards.keyboards import (
create_keyboard_for_approve_ban,
create_keyboard_for_ban_days,
create_keyboard_for_ban_reason,
create_keyboard_with_pagination,
get_reply_keyboard_admin,
)
from helper_bot.handlers.admin.utils import (escape_html,
format_ban_confirmation,
format_user_info,
handle_admin_error,
return_to_admin_menu)
from helper_bot.keyboards.keyboards import (create_keyboard_for_approve_ban,
create_keyboard_for_ban_days,
create_keyboard_for_ban_reason,
create_keyboard_with_pagination,
get_reply_keyboard_admin)
from helper_bot.utils.base_dependency_factory import get_global_instance
# Local imports - metrics
from helper_bot.utils.metrics import db_query_time, track_errors, track_time
from logs.custom_logger import logger
@@ -37,19 +30,23 @@ admin_router.message.middleware(AdminAccessMiddleware())
# ХЕНДЛЕРЫ МЕНЮ
# ============================================================================
@admin_router.message(ChatTypeFilter(chat_type=["private"]), Command("admin"))
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
Command('admin')
)
@track_time("admin_panel", "admin_handlers")
@track_errors("admin_handlers", "admin_panel")
async def admin_panel(message: types.Message, state: FSMContext, **kwargs):
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
)
await message.answer("Добро пожаловать в админку. Выбери что хочешь:", reply_markup=markup)
except Exception as e:
await handle_admin_error(message, e, state, "admin_panel")
@@ -58,20 +55,18 @@ async def admin_panel(message: types.Message, state: FSMContext, **kwargs):
# ХЕНДЛЕР ОТМЕНЫ
# ============================================================================
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter(
"AWAIT_BAN_TARGET",
"AWAIT_BAN_DETAILS",
"AWAIT_BAN_DURATION",
"BAN_CONFIRMATION",
),
F.text == "Отменить",
StateFilter("AWAIT_BAN_TARGET", "AWAIT_BAN_DETAILS", "AWAIT_BAN_DURATION", "BAN_CONFIRMATION"),
F.text == 'Отменить'
)
@track_time("cancel_ban_process", "admin_handlers")
@track_errors("admin_handlers", "cancel_ban_process")
async def cancel_ban_process(message: types.Message, state: FSMContext, **kwargs):
async def cancel_ban_process(
message: types.Message,
state: FSMContext,
**kwargs
):
"""Отмена процесса блокировки"""
try:
current_state = await state.get_state()
@@ -84,31 +79,32 @@ async def cancel_ban_process(message: types.Message, state: FSMContext, **kwargs
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"),
F.text == "Бан (Список)",
F.text == 'Бан (Список)'
)
@track_time("get_last_users", "admin_handlers")
@track_errors("admin_handlers", "get_last_users")
@db_query_time("get_last_users", "users", "select")
async def get_last_users(
message: types.Message, state: FSMContext, bot_db: MagicData("bot_db")
):
message: types.Message,
state: FSMContext,
bot_db: MagicData("bot_db")
):
"""Получение списка последних пользователей"""
try:
logger.info(
f"Получение списка последних пользователей. Пользователь: {message.from_user.full_name}"
)
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"
)
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,
reply_markup=keyboard
)
except Exception as e:
await handle_admin_error(message, e, state, "get_last_users")
@@ -117,113 +113,184 @@ async def get_last_users(
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"),
F.text == "Разбан (список)",
F.text == 'Разбан (список)'
)
@track_time("get_banned_users", "admin_handlers")
@track_errors("admin_handlers", "get_banned_users")
@db_query_time("get_banned_users", "users", "select")
async def get_banned_users(
message: types.Message, state: FSMContext, bot_db: MagicData("bot_db")
):
message: types.Message,
state: FSMContext,
bot_db: MagicData("bot_db")
):
"""Получение списка заблокированных пользователей"""
try:
logger.info(
f"Получение списка заблокированных пользователей. Пользователь: {message.from_user.full_name}"
)
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"
)
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="В списке заблокированных пользователей никого нет"
)
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 == '📊 ML Статистика'
)
@track_time("get_ml_stats", "admin_handlers")
@track_errors("admin_handlers", "get_ml_stats")
async def get_ml_stats(
message: types.Message,
state: FSMContext,
**kwargs
):
"""Получение статистики ML-скоринга"""
try:
logger.info(f"Запрос ML статистики от пользователя: {message.from_user.full_name}")
bdf = get_global_instance()
scoring_manager = bdf.get_scoring_manager()
if not scoring_manager:
await message.answer("📊 ML Scoring отключен\n\nДля включения установите RAG_ENABLED=true или DEEPSEEK_ENABLED=true в .env")
return
stats = await scoring_manager.get_stats()
# Формируем текст статистики
lines = ["📊 <b>ML Scoring Статистика</b>\n"]
# RAG статистика
if "rag" in stats:
rag = stats["rag"]
lines.append("🤖 <b>RAG API:</b>")
# Проверяем, есть ли данные из API (новый контракт содержит model_loaded и vector_store)
if "model_loaded" in rag or "vector_store" in rag:
# Данные из API /stats
if "model_loaded" in rag:
model_loaded = rag.get('model_loaded', False)
lines.append(f" • Модель загружена: {'' if model_loaded else ''}")
if "model_name" in rag:
lines.append(f" • Модель: {rag.get('model_name', 'N/A')}")
if "device" in rag:
lines.append(f" • Устройство: {rag.get('device', 'N/A')}")
# Статистика из vector_store
if "vector_store" in rag:
vector_store = rag["vector_store"]
positive_count = vector_store.get("positive_count", 0)
negative_count = vector_store.get("negative_count", 0)
total_count = vector_store.get("total_count", 0)
lines.append(f" • Положительных примеров: {positive_count}")
lines.append(f" • Отрицательных примеров: {negative_count}")
lines.append(f"Всего примеров: {total_count}")
if "vector_dim" in vector_store:
lines.append(f" • Размерность векторов: {vector_store.get('vector_dim', 'N/A')}")
if "max_examples" in vector_store:
lines.append(f" • Макс. примеров: {vector_store.get('max_examples', 'N/A')}")
else:
# Fallback на синхронные данные (если API недоступен)
lines.append(f" • API URL: {rag.get('api_url', 'N/A')}")
if "enabled" in rag:
lines.append(f" • Статус: {'✅ Включен' if rag.get('enabled') else '❌ Отключен'}")
lines.append("")
# DeepSeek статистика
if "deepseek" in stats:
ds = stats["deepseek"]
lines.append("🔮 <b>DeepSeek API:</b>")
lines.append(f" • Статус: {'✅ Включен' if ds.get('enabled') else '❌ Отключен'}")
lines.append(f" • Модель: {ds.get('model', 'N/A')}")
lines.append(f" • Таймаут: {ds.get('timeout', 'N/A')}с")
lines.append("")
# Если ничего не включено
if "rag" not in stats and "deepseek" not in stats:
lines.append("⚠️ Ни один сервис не настроен")
await message.answer("\n".join(lines), parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка получения ML статистики: {e}")
await message.answer(f"❌ Ошибка получения статистики: {str(e)}")
# ============================================================================
# ХЕНДЛЕРЫ ПРОЦЕССА БАНА
# ============================================================================
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"),
F.text.in_(["Бан по нику", "Бан по ID"]),
F.text.in_(['Бан по нику', 'Бан по ID'])
)
@track_time("start_ban_process", "admin_handlers")
@track_errors("admin_handlers", "start_ban_process")
async def start_ban_process(message: types.Message, state: FSMContext, **kwargs):
async def start_ban_process(
message: types.Message,
state: FSMContext,
**kwargs
):
"""Начало процесса блокировки пользователя"""
try:
ban_type = "username" if message.text == "Бан по нику" else "id"
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 блокируемого пользователя"
)
prompt_text = "Пришли мне username блокируемого пользователя" if ban_type == "username" else "Пришли мне ID блокируемого пользователя"
await message.answer(prompt_text)
await state.set_state("AWAIT_BAN_TARGET")
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")
ChatTypeFilter(chat_type=["private"]),
StateFilter("AWAIT_BAN_TARGET")
)
@track_time("process_ban_target", "admin_handlers")
@track_errors("admin_handlers", "process_ban_target")
async def process_ban_target(
message: types.Message, state: FSMContext, bot_db: MagicData("bot_db")
):
message: types.Message,
state: FSMContext,
bot_db: MagicData("bot_db")
):
"""Обработка введенного username/ID для блокировки"""
logger.info(
f"process_ban_target: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}"
)
logger.info(f"process_ban_target: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}")
try:
user_data = await state.get_data()
ban_type = user_data.get("ban_type")
ban_type = user_data.get('ban_type')
admin_service = AdminService(bot_db)
logger.info(f"process_ban_target: ban_type={ban_type}, user_data={user_data}")
# Определяем пользователя
if ban_type == "username":
logger.info(
f"process_ban_target: Поиск пользователя по username: {message.text}"
)
logger.info(f"process_ban_target: Поиск пользователя по username: {message.text}")
user = await admin_service.get_user_by_username(message.text)
if not user:
logger.warning(
f"process_ban_target: Пользователь с username '{message.text}' не найден"
)
await message.answer(
f"Пользователь с username '{escape_html(message.text)}' не найден."
)
logger.warning(f"process_ban_target: Пользователь с username '{message.text}' не найден")
await message.answer(f"Пользователь с username '{escape_html(message.text)}' не найден.")
await return_to_admin_menu(message, state)
return
else: # ban_type == "id"
try:
logger.info(
f"process_ban_target: Валидация и поиск пользователя по ID: {message.text}"
)
logger.info(f"process_ban_target: Валидация и поиск пользователя по ID: {message.text}")
user_id = await admin_service.validate_user_input(message.text)
user = await admin_service.get_user_by_id(user_id)
if not user:
logger.warning(
f"process_ban_target: Пользователь с ID {user_id} не найден в базе данных"
)
await message.answer(
f"Пользователь с ID {user_id} не найден в базе данных."
)
logger.warning(f"process_ban_target: Пользователь с ID {user_id} не найден в базе данных")
await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
await return_to_admin_menu(message, state)
return
except InvalidInputError as e:
@@ -231,117 +298,115 @@ async def process_ban_target(
await message.answer(str(e))
await return_to_admin_menu(message, state)
return
logger.info(
f"process_ban_target: Найден пользователь: {user.user_id}, {user.username}, {user.full_name}"
)
logger.info(f"process_ban_target: Найден пользователь: {user.user_id}, {user.username}, {user.full_name}")
# Сохраняем данные пользователя
await state.update_data(
target_user_id=user.user_id,
target_username=user.username,
target_full_name=user.full_name,
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()
logger.info(
f"process_ban_target: Отправка сообщения с причиной бана, user_info: {user_info}"
)
logger.info(f"process_ban_target: Отправка сообщения с причиной бана, user_info: {user_info}")
await message.answer(
text=f"{user_info}\n\nВыбери причину бана из списка или напиши ее в чат",
reply_markup=markup,
reply_markup=markup
)
await state.set_state("AWAIT_BAN_DETAILS")
await state.set_state('AWAIT_BAN_DETAILS')
logger.info("process_ban_target: Состояние изменено на AWAIT_BAN_DETAILS")
except Exception as e:
logger.error(f"process_ban_target: Неожиданная ошибка: {e}", exc_info=True)
await handle_admin_error(message, e, state, "process_ban_target")
@admin_router.message(
ChatTypeFilter(chat_type=["private"]), StateFilter("AWAIT_BAN_DETAILS")
ChatTypeFilter(chat_type=["private"]),
StateFilter("AWAIT_BAN_DETAILS")
)
@track_time("process_ban_reason", "admin_handlers")
@track_errors("admin_handlers", "process_ban_reason")
async def process_ban_reason(message: types.Message, state: FSMContext, **kwargs):
async def process_ban_reason(
message: types.Message,
state: FSMContext,
**kwargs
):
"""Обработка причины блокировки"""
logger.info(
f"process_ban_reason: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}"
)
logger.info(f"process_ban_reason: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}")
try:
# Проверяем текущее состояние
current_state = await state.get_state()
logger.info(f"process_ban_reason: Текущее состояние: {current_state}")
# Проверяем данные состояния
state_data = await state.get_data()
logger.info(f"process_ban_reason: Данные состояния: {state_data}")
logger.info(
f"process_ban_reason: Обновление данных состояния с причиной: {message.text}"
)
logger.info(f"process_ban_reason: Обновление данных состояния с причиной: {message.text}")
await state.update_data(ban_reason=message.text)
markup = create_keyboard_for_ban_days()
safe_reason = escape_html(message.text)
logger.info(
f"process_ban_reason: Отправка сообщения с выбором срока бана, причина: {safe_reason}"
)
logger.info(f"process_ban_reason: Отправка сообщения с выбором срока бана, причина: {safe_reason}")
await message.answer(
f"Выбрана причина: {safe_reason}. Выбери срок бана в днях или напиши его в чат",
reply_markup=markup,
reply_markup=markup
)
await state.set_state("AWAIT_BAN_DURATION")
await state.set_state('AWAIT_BAN_DURATION')
logger.info("process_ban_reason: Состояние изменено на AWAIT_BAN_DURATION")
except Exception as e:
logger.error(f"process_ban_reason: Неожиданная ошибка: {e}", exc_info=True)
await handle_admin_error(message, e, state, "process_ban_reason")
@admin_router.message(
ChatTypeFilter(chat_type=["private"]), StateFilter("AWAIT_BAN_DURATION")
ChatTypeFilter(chat_type=["private"]),
StateFilter("AWAIT_BAN_DURATION")
)
@track_time("process_ban_duration", "admin_handlers")
@track_errors("admin_handlers", "process_ban_duration")
async def process_ban_duration(message: types.Message, state: FSMContext, **kwargs):
async def process_ban_duration(
message: types.Message,
state: FSMContext,
**kwargs
):
"""Обработка срока блокировки"""
try:
user_data = await state.get_data()
# Определяем срок блокировки
if message.text == "Навсегда":
if message.text == 'Навсегда':
ban_days = None
else:
try:
ban_days = int(message.text)
if ban_days <= 0:
await message.answer(
"Срок блокировки должен быть положительным числом."
)
await message.answer("Срок блокировки должен быть положительным числом.")
return
except ValueError:
await message.answer(
"Пожалуйста, введите корректное число дней или выберите 'Навсегда'."
)
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
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")
await state.set_state('BAN_CONFIRMATION')
except Exception as e:
await handle_admin_error(message, e, state, "process_ban_duration")
@@ -349,31 +414,35 @@ async def process_ban_duration(message: types.Message, state: FSMContext, **kwar
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("BAN_CONFIRMATION"),
F.text == "Подтвердить",
F.text == 'Подтвердить'
)
@track_time("confirm_ban", "admin_handlers")
@track_errors("admin_handlers", "confirm_ban")
async def confirm_ban(
message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), **kwargs
):
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"],
user_id=user_data['target_user_id'],
username=user_data['target_username'],
reason=user_data['ban_reason'],
ban_days=user_data['ban_days'],
ban_author_id=message.from_user.id,
)
safe_username = escape_html(user_data["target_username"])
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)

View File

@@ -7,7 +7,6 @@ except ImportError:
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from helper_bot.utils.base_dependency_factory import get_global_instance
from helper_bot.utils.helper_func import check_access
from logs.custom_logger import logger

View File

@@ -1,31 +1,25 @@
"""
Обработчики команд для мониторинга rate limiting
"""
from aiogram import F, Router, types
from aiogram.filters import Command, MagicData
from aiogram.fsm.context import FSMContext
from aiogram.types import FSInputFile
from helper_bot.filters.main import ChatTypeFilter
from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware
from helper_bot.middlewares.dependencies_middleware import \
DependenciesMiddleware
# Local imports - metrics
from helper_bot.utils.metrics import track_errors, track_time
from helper_bot.utils.rate_limit_metrics import (
get_rate_limit_metrics_summary,
update_rate_limit_gauges,
)
from helper_bot.utils.rate_limit_monitor import (
get_rate_limit_summary,
rate_limit_monitor,
)
get_rate_limit_metrics_summary, update_rate_limit_gauges)
from helper_bot.utils.rate_limit_monitor import (get_rate_limit_summary,
rate_limit_monitor)
from logs.custom_logger import logger
class RateLimitHandlers:
def __init__(self, db, settings):
self.db = db.get_db() if hasattr(db, "get_db") else db
self.db = db.get_db() if hasattr(db, 'get_db') else db
self.settings = settings
self.router = Router()
self._setup_handlers()
@@ -39,38 +33,38 @@ class RateLimitHandlers:
self.router.message.register(
self.rate_limit_stats_handler,
ChatTypeFilter(chat_type=["private"]),
Command("ratelimit_stats"),
Command("ratelimit_stats")
)
# Команда для сброса статистики rate limiting
self.router.message.register(
self.reset_rate_limit_stats_handler,
ChatTypeFilter(chat_type=["private"]),
Command("reset_ratelimit_stats"),
Command("reset_ratelimit_stats")
)
# Команда для просмотра ошибок rate limiting
self.router.message.register(
self.rate_limit_errors_handler,
ChatTypeFilter(chat_type=["private"]),
Command("ratelimit_errors"),
Command("ratelimit_errors")
)
# Команда для просмотра Prometheus метрик
self.router.message.register(
self.rate_limit_prometheus_handler,
ChatTypeFilter(chat_type=["private"]),
Command("ratelimit_prometheus"),
Command("ratelimit_prometheus")
)
@track_time("rate_limit_stats_handler", "rate_limit_handlers")
@track_errors("rate_limit_handlers", "rate_limit_stats_handler")
async def rate_limit_stats_handler(
self,
message: types.Message,
state: FSMContext,
message: types.Message,
state: FSMContext,
bot_db: MagicData("bot_db"),
settings: MagicData("settings"),
settings: MagicData("settings")
):
"""Показывает статистику rate limiting"""
try:
@@ -78,11 +72,11 @@ class RateLimitHandlers:
if not await bot_db.is_admin(message.from_user.id):
await message.answer("У вас нет прав для выполнения этой команды.")
return
# Получаем сводку
summary = get_rate_limit_summary()
global_stats = rate_limit_monitor.get_global_stats()
# Формируем сообщение со статистикой
stats_text = (
f"📊 <b>Статистика Rate Limiting</b>\n\n"
@@ -95,17 +89,15 @@ class RateLimitHandlers:
f"• Активных чатов: {summary['active_chats']}\n"
f"• Ошибок за час: {summary['recent_errors_count']}\n\n"
)
# Добавляем детальную статистику
stats_text += f"🔍 <b>Детальная статистика:</b>\n"
stats_text += f"• Успешных запросов: {global_stats.successful_requests}\n"
stats_text += f"• Неудачных запросов: {global_stats.failed_requests}\n"
stats_text += f"• RetryAfter ошибок: {global_stats.retry_after_errors}\n"
stats_text += f"• Других ошибок: {global_stats.other_errors}\n"
stats_text += (
f"• Общее время ожидания: {global_stats.total_wait_time:.2f}с\n\n"
)
stats_text += f"• Общее время ожидания: {global_stats.total_wait_time:.2f}с\n\n"
# Добавляем топ чатов по запросам
top_chats = rate_limit_monitor.get_top_chats_by_requests(5)
if top_chats:
@@ -113,16 +105,16 @@ class RateLimitHandlers:
for i, (chat_id, chat_stats) in enumerate(top_chats, 1):
stats_text += f"{i}. Chat {chat_id}: {chat_stats.total_requests} запросов ({chat_stats.success_rate:.1%} успех)\n"
stats_text += "\n"
# Добавляем чаты с высоким процентом ошибок
high_error_chats = rate_limit_monitor.get_chats_with_high_error_rate(0.1)
if high_error_chats:
stats_text += f"⚠️ <b>Чаты с высоким процентом ошибок (>10%):</b>\n"
for chat_id, chat_stats in high_error_chats[:3]:
stats_text += f"• Chat {chat_id}: {chat_stats.error_rate:.1%} ошибок ({chat_stats.failed_requests}/{chat_stats.total_requests})\n"
await message.answer(stats_text, parse_mode="HTML")
await message.answer(stats_text, parse_mode='HTML')
except Exception as e:
logger.error(f"Ошибка при получении статистики rate limiting: {e}")
await message.answer("Произошла ошибка при получении статистики.")
@@ -131,10 +123,10 @@ class RateLimitHandlers:
@track_errors("rate_limit_handlers", "reset_rate_limit_stats_handler")
async def reset_rate_limit_stats_handler(
self,
message: types.Message,
state: FSMContext,
message: types.Message,
state: FSMContext,
bot_db: MagicData("bot_db"),
settings: MagicData("settings"),
settings: MagicData("settings")
):
"""Сбрасывает статистику rate limiting"""
try:
@@ -142,12 +134,12 @@ class RateLimitHandlers:
if not await bot_db.is_admin(message.from_user.id):
await message.answer("У вас нет прав для выполнения этой команды.")
return
# Сбрасываем статистику
rate_limit_monitor.reset_stats()
await message.answer("✅ Статистика rate limiting сброшена.")
except Exception as e:
logger.error(f"Ошибка при сбросе статистики rate limiting: {e}")
await message.answer("Произошла ошибка при сбросе статистики.")
@@ -156,10 +148,10 @@ class RateLimitHandlers:
@track_errors("rate_limit_handlers", "rate_limit_errors_handler")
async def rate_limit_errors_handler(
self,
message: types.Message,
state: FSMContext,
message: types.Message,
state: FSMContext,
bot_db: MagicData("bot_db"),
settings: MagicData("settings"),
settings: MagicData("settings")
):
"""Показывает недавние ошибки rate limiting"""
try:
@@ -167,34 +159,29 @@ class RateLimitHandlers:
if not await bot_db.is_admin(message.from_user.id):
await message.answer("У вас нет прав для выполнения этой команды.")
return
# Получаем ошибки за последний час
recent_errors = rate_limit_monitor.get_recent_errors(60)
error_summary = rate_limit_monitor.get_error_summary(60)
if not recent_errors:
await message.answer(
"✅ Ошибок rate limiting за последний час не было."
)
await message.answer("✅ Ошибок rate limiting за последний час не было.")
return
# Формируем сообщение с ошибками
errors_text = f"🚨 <b>Ошибки Rate Limiting (последний час)</b>\n\n"
errors_text += f"📊 <b>Сводка ошибок:</b>\n"
for error_type, count in error_summary.items():
errors_text += f"{error_type}: {count}\n"
errors_text += f"\nВсего ошибок: {len(recent_errors)}\n\n"
# Показываем последние 10 ошибок
errors_text += f"🔍 <b>Последние ошибки:</b>\n"
for i, error in enumerate(recent_errors[-10:], 1):
from datetime import datetime
timestamp = datetime.fromtimestamp(error["timestamp"]).strftime(
"%H:%M:%S"
)
timestamp = datetime.fromtimestamp(error['timestamp']).strftime("%H:%M:%S")
errors_text += f"{i}. {timestamp} - Chat {error['chat_id']} - {error['error_type']}\n"
# Если сообщение слишком длинное, разбиваем на части
if len(errors_text) > 4000:
# Отправляем сводку
@@ -203,37 +190,32 @@ class RateLimitHandlers:
for error_type, count in error_summary.items():
summary_text += f"{error_type}: {count}\n"
summary_text += f"\nВсего ошибок: {len(recent_errors)}"
await message.answer(summary_text, parse_mode="HTML")
await message.answer(summary_text, parse_mode='HTML')
# Отправляем детали отдельным сообщением
details_text = f"🔍 <b>Последние ошибки:</b>\n"
for i, error in enumerate(recent_errors[-10:], 1):
from datetime import datetime
timestamp = datetime.fromtimestamp(error["timestamp"]).strftime(
"%H:%M:%S"
)
timestamp = datetime.fromtimestamp(error['timestamp']).strftime("%H:%M:%S")
details_text += f"{i}. {timestamp} - Chat {error['chat_id']} - {error['error_type']}\n"
await message.answer(details_text, parse_mode="HTML")
await message.answer(details_text, parse_mode='HTML')
else:
await message.answer(errors_text, parse_mode="HTML")
await message.answer(errors_text, parse_mode='HTML')
except Exception as e:
logger.error(f"Ошибка при получении ошибок rate limiting: {e}")
await message.answer(
"Произошла ошибка при получении информации об ошибках."
)
await message.answer("Произошла ошибка при получении информации об ошибках.")
@track_time("rate_limit_prometheus_handler", "rate_limit_handlers")
@track_errors("rate_limit_handlers", "rate_limit_prometheus_handler")
async def rate_limit_prometheus_handler(
self,
message: types.Message,
state: FSMContext,
message: types.Message,
state: FSMContext,
bot_db: MagicData("bot_db"),
settings: MagicData("settings"),
settings: MagicData("settings")
):
"""Показывает Prometheus метрики rate limiting"""
try:
@@ -241,13 +223,13 @@ class RateLimitHandlers:
if not await bot_db.is_admin(message.from_user.id):
await message.answer("У вас нет прав для выполнения этой команды.")
return
# Обновляем gauge метрики
update_rate_limit_gauges()
# Получаем сводку метрик
metrics_summary = get_rate_limit_metrics_summary()
# Формируем сообщение с метриками
metrics_text = (
f"📊 <b>Prometheus метрики Rate Limiting</b>\n\n"
@@ -259,40 +241,30 @@ class RateLimitHandlers:
f"• rate_limit_avg_wait_time: {metrics_summary['average_wait_time']:.3f}s\n"
f"• rate_limit_active_chats: {metrics_summary['active_chats']}\n\n"
)
# Добавляем детальные метрики
metrics_text += f"🔍 <b>Детальные метрики:</b>\n"
metrics_text += (
f"Успешных запросов: {metrics_summary['successful_requests']}\n"
)
metrics_text += (
f"• Неудачных запросов: {metrics_summary['failed_requests']}\n"
)
metrics_text += (
f"• RetryAfter ошибок: {metrics_summary['retry_after_errors']}\n"
)
metrics_text += f"• Успешных запросов: {metrics_summary['successful_requests']}\n"
metrics_text += f"Неудачных запросов: {metrics_summary['failed_requests']}\n"
metrics_text += f"• RetryAfter ошибок: {metrics_summary['retry_after_errors']}\n"
metrics_text += f"• Других ошибок: {metrics_summary['other_errors']}\n"
metrics_text += (
f"• Общее время ожидания: {metrics_summary['total_wait_time']:.2f}s\n\n"
)
metrics_text += f"• Общее время ожидания: {metrics_summary['total_wait_time']:.2f}s\n\n"
# Добавляем информацию о доступных метриках
metrics_text += f"📈 <b>Доступные Prometheus метрики:</b>\n"
metrics_text += f"• rate_limit_requests_total - общее количество запросов\n"
metrics_text += f"• rate_limit_errors_total - количество ошибок по типам\n"
metrics_text += f"• rate_limit_wait_duration_seconds - время ожидания\n"
metrics_text += (
f"• rate_limit_request_interval_seconds - интервалы между запросами\n"
)
metrics_text += f"• rate_limit_request_interval_seconds - интервалы между запросами\n"
metrics_text += f"• rate_limit_active_chats - количество активных чатов\n"
metrics_text += f"• rate_limit_success_rate - процент успеха по чатам\n"
metrics_text += f"• rate_limit_requests_per_minute - запросов в минуту\n"
metrics_text += f"• rate_limit_total_requests - общее количество запросов\n"
metrics_text += f"• rate_limit_total_errors - количество ошибок\n"
metrics_text += f"• rate_limit_avg_wait_time - среднее время ожидания\n"
await message.answer(metrics_text, parse_mode="HTML")
await message.answer(metrics_text, parse_mode='HTML')
except Exception as e:
logger.error(f"Ошибка при получении Prometheus метрик: {e}")
await message.answer("Произошла ошибка при получении метрик.")

View File

@@ -1,16 +1,11 @@
from datetime import datetime
from typing import List, Optional
from helper_bot.handlers.admin.exceptions import (
InvalidInputError,
UserAlreadyBannedError,
)
from helper_bot.utils.helper_func import (
add_days_to_date,
get_banned_users_buttons,
get_banned_users_list,
)
from helper_bot.handlers.admin.exceptions import (InvalidInputError,
UserAlreadyBannedError)
from helper_bot.utils.helper_func import (add_days_to_date,
get_banned_users_buttons,
get_banned_users_list)
# Local imports - metrics
from helper_bot.utils.metrics import track_errors, track_time
from logs.custom_logger import logger
@@ -18,7 +13,6 @@ from logs.custom_logger import logger
class User:
"""Модель пользователя"""
def __init__(self, user_id: int, username: str, full_name: str):
self.user_id = user_id
self.username = username
@@ -27,10 +21,7 @@ class User:
class BannedUser:
"""Модель заблокированного пользователя"""
def __init__(
self, user_id: int, username: str, reason: str, unban_date: Optional[datetime]
):
def __init__(self, user_id: int, username: str, reason: str, unban_date: Optional[datetime]):
self.user_id = user_id
self.username = username
self.reason = reason
@@ -39,10 +30,10 @@ class BannedUser:
class AdminService:
"""Сервис для административных операций"""
def __init__(self, bot_db):
self.bot_db = bot_db
@track_time("get_last_users", "admin_service")
@track_errors("admin_service", "get_last_users")
async def get_last_users(self) -> List[User]:
@@ -50,13 +41,17 @@ class AdminService:
try:
users_data = await self.bot_db.get_last_users(30)
return [
User(user_id=user[1], username="Неизвестно", full_name=user[0])
User(
user_id=user[1],
username='Неизвестно',
full_name=user[0]
)
for user in users_data
]
except Exception as e:
logger.error(f"Ошибка при получении списка последних пользователей: {e}")
raise
@track_time("get_banned_users", "admin_service")
@track_errors("admin_service", "get_banned_users")
async def get_banned_users(self) -> List[BannedUser]:
@@ -70,22 +65,18 @@ class AdminService:
username = await self.bot_db.get_username(user_id)
full_name = await self.bot_db.get_full_name_by_id(user_id)
user_name = username or full_name or f"User_{user_id}"
banned_users.append(
BannedUser(
user_id=user_id,
username=user_name,
reason=reason,
unban_date=unban_date,
)
)
banned_users.append(BannedUser(
user_id=user_id,
username=user_name,
reason=reason,
unban_date=unban_date
))
return banned_users
except Exception as e:
logger.error(
f"Ошибка при получении списка заблокированных пользователей: {e}"
)
logger.error(f"Ошибка при получении списка заблокированных пользователей: {e}")
raise
@track_time("get_user_by_username", "admin_service")
@track_errors("admin_service", "get_user_by_username")
async def get_user_by_username(self, username: str) -> Optional[User]:
@@ -94,15 +85,17 @@ class AdminService:
user_id = await self.bot_db.get_user_id_by_username(username)
if not user_id:
return None
full_name = await self.bot_db.get_full_name_by_id(user_id)
return User(
user_id=user_id, username=username, full_name=full_name or "Неизвестно"
user_id=user_id,
username=username,
full_name=full_name or 'Неизвестно'
)
except Exception as e:
logger.error(f"Ошибка при поиске пользователя по username {username}: {e}")
raise
@track_time("get_user_by_id", "admin_service")
@track_errors("admin_service", "get_user_by_id")
async def get_user_by_id(self, user_id: int) -> Optional[User]:
@@ -111,50 +104,39 @@ class AdminService:
user_info = await self.bot_db.get_user_by_id(user_id)
if not user_info:
return None
return User(
user_id=user_id,
username=user_info.username or "Неизвестно",
full_name=user_info.full_name or "Неизвестно",
username=user_info.username or 'Неизвестно',
full_name=user_info.full_name or 'Неизвестно'
)
except Exception as e:
logger.error(f"Ошибка при поиске пользователя по ID {user_id}: {e}")
raise
@track_time("ban_user", "admin_service")
@track_errors("admin_service", "ban_user")
async def ban_user(
self,
user_id: int,
username: str,
reason: str,
ban_days: Optional[int],
ban_author_id: int,
) -> None:
async def ban_user(self, user_id: int, username: str, reason: str, ban_days: Optional[int], ban_author_id: int) -> None:
"""Заблокировать пользователя"""
try:
# Проверяем, не заблокирован ли уже пользователь
if await self.bot_db.check_user_in_blacklist(user_id):
raise UserAlreadyBannedError(f"Пользователь {user_id} уже заблокирован")
# Рассчитываем дату разблокировки
date_to_unban = None
if ban_days is not None:
date_to_unban = add_days_to_date(ban_days)
# Сохраняем в БД (username больше не передается, так как не используется в новой схеме)
await self.bot_db.set_user_blacklist(
user_id, None, reason, date_to_unban, ban_author=ban_author_id
)
logger.info(
f"Пользователь {user_id} ({username}) заблокирован. Причина: {reason}, срок: {ban_days} дней"
)
await self.bot_db.set_user_blacklist(user_id, None, reason, date_to_unban, ban_author=ban_author_id)
logger.info(f"Пользователь {user_id} ({username}) заблокирован. Причина: {reason}, срок: {ban_days} дней")
except Exception as e:
logger.error(f"Ошибка при блокировке пользователя {user_id}: {e}")
raise
@track_time("unban_user", "admin_service")
@track_errors("admin_service", "unban_user")
async def unban_user(self, user_id: int) -> None:
@@ -165,7 +147,7 @@ class AdminService:
except Exception as e:
logger.error(f"Ошибка при разблокировке пользователя {user_id}: {e}")
raise
@track_time("validate_user_input", "admin_service")
@track_errors("admin_service", "validate_user_input")
async def validate_user_input(self, input_text: str) -> int:
@@ -173,13 +155,11 @@ class AdminService:
try:
user_id = int(input_text.strip())
if user_id <= 0:
raise InvalidInputError(
"ID пользователя должен быть положительным числом"
)
raise InvalidInputError("ID пользователя должен быть положительным числом")
return user_id
except ValueError:
raise InvalidInputError("ID пользователя должен быть числом")
@track_time("get_banned_users_for_display", "admin_service")
@track_errors("admin_service", "get_banned_users_for_display")
async def get_banned_users_for_display(self, page: int = 0) -> tuple[str, list]:
@@ -190,7 +170,5 @@ class AdminService:
buttons_list = await get_banned_users_buttons(self.bot_db)
return message_text, buttons_list
except Exception as e:
logger.error(
f"Ошибка при получении данных заблокированных пользователей: {e}"
)
logger.error(f"Ошибка при получении данных заблокированных пользователей: {e}")
raise

View File

@@ -3,7 +3,6 @@ from typing import Optional
from aiogram import types
from aiogram.fsm.context import FSMContext
from helper_bot.handlers.admin.exceptions import AdminError
from helper_bot.keyboards.keyboards import get_reply_keyboard_admin
from logs.custom_logger import logger
@@ -14,41 +13,33 @@ def escape_html(text: str) -> str:
return html.escape(str(text)) if text else ""
async def return_to_admin_menu(
message: types.Message, state: FSMContext, additional_message: Optional[str] = None
) -> None:
async def return_to_admin_menu(message: types.Message, state: FSMContext,
additional_message: Optional[str] = None) -> None:
"""Универсальная функция для возврата в админ-меню"""
logger.info(
f"return_to_admin_menu: Возврат в админ-меню для пользователя {message.from_user.id}"
)
logger.info(f"return_to_admin_menu: Возврат в админ-меню для пользователя {message.from_user.id}")
await state.set_data({})
await state.set_state("ADMIN")
markup = get_reply_keyboard_admin()
if additional_message:
logger.info(
f"return_to_admin_menu: Отправка дополнительного сообщения: {additional_message}"
)
logger.info(f"return_to_admin_menu: Отправка дополнительного сообщения: {additional_message}")
await message.answer(additional_message)
await message.answer("Вернулись в меню", reply_markup=markup)
logger.info(
f"return_to_admin_menu: Пользователь {message.from_user.id} успешно возвращен в админ-меню"
)
await message.answer('Вернулись в меню', reply_markup=markup)
logger.info(f"return_to_admin_menu: Пользователь {message.from_user.id} успешно возвращен в админ-меню")
async def handle_admin_error(
message: types.Message, error: Exception, state: FSMContext, error_context: str = ""
) -> None:
async def handle_admin_error(message: types.Message, error: Exception,
state: FSMContext, error_context: str = "") -> None:
"""Централизованная обработка ошибок административных операций"""
logger.error(f"Ошибка в {error_context}: {error}")
if isinstance(error, AdminError):
await message.answer(f"Ошибка: {str(error)}")
else:
await message.answer("Произошла внутренняя ошибка. Попробуйте позже.")
await return_to_admin_menu(message, state)
@@ -56,23 +47,19 @@ def format_user_info(user_id: int, username: str, full_name: str) -> str:
"""Форматирование информации о пользователе для отображения"""
safe_username = escape_html(username)
safe_full_name = escape_html(full_name)
return (
f"<b>Выбран пользователь:</b>\n"
f"<b>ID:</b> {user_id}\n"
f"<b>Username:</b> {safe_username}\n"
f"<b>Имя:</b> {safe_full_name}"
)
return (f"<b>Выбран пользователь:</b>\n"
f"<b>ID:</b> {user_id}\n"
f"<b>Username:</b> {safe_username}\n"
f"<b>Имя:</b> {safe_full_name}")
def format_ban_confirmation(user_id: int, reason: str, ban_days: Optional[int]) -> str:
"""Форматирование подтверждения бана"""
safe_reason = escape_html(reason)
ban_text = "Навсегда" if ban_days is None else f"{ban_days} дней"
return (
f"<b>Необходимо подтверждение:</b>\n"
f"<b>Пользователь:</b> {user_id}\n"
f"<b>Причина бана:</b> {safe_reason}\n"
f"<b>Срок бана:</b> {ban_text}"
)
return (f"<b>Необходимо подтверждение:</b>\n"
f"<b>Пользователь:</b> {user_id}\n"
f"<b>Причина бана:</b> {safe_reason}\n"
f"<b>Срок бана:</b> {ban_text}")