590 lines
24 KiB
Python
590 lines
24 KiB
Python
"""
|
||
Обработчики команды статистики
|
||
"""
|
||
from datetime import datetime
|
||
from aiogram import Router, F
|
||
from aiogram.filters import Command
|
||
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
|
||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||
|
||
from bot.filters.admin import IsAdmin
|
||
from configs import settings, COMMANDS
|
||
from database import get_manager
|
||
from middleware.loggers import logger
|
||
from bot.utils.decorators import log_action
|
||
|
||
__all__ = ("router",)
|
||
|
||
router: Router = Router(name="stats_router")
|
||
|
||
|
||
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
|
||
|
||
def format_number(num: int) -> str:
|
||
"""Форматирует большие числа с разделителями"""
|
||
return f"{num:,}".replace(",", " ")
|
||
|
||
|
||
def create_text_bar(value: int, max_value: int, length: int = 10) -> str:
|
||
"""Создает текстовую полоску прогресса"""
|
||
if max_value == 0:
|
||
return "░" * length
|
||
|
||
filled = int((value / max_value) * length)
|
||
filled = max(0, min(filled, length))
|
||
empty = length - filled
|
||
|
||
return "█" * filled + "░" * empty
|
||
|
||
|
||
def format_datetime(dt: datetime) -> str:
|
||
"""Форматирует datetime в читабельный формат"""
|
||
return dt.strftime("%d.%m.%Y %H:%M")
|
||
|
||
|
||
def format_time_remaining(minutes: int) -> str:
|
||
"""
|
||
Форматирует оставшееся время в читабельный формат.
|
||
|
||
Args:
|
||
minutes: Количество минут
|
||
|
||
Returns:
|
||
Отформатированная строка времени
|
||
"""
|
||
if minutes <= 0:
|
||
return "истёк"
|
||
elif minutes < 60:
|
||
return f"{minutes} мин"
|
||
elif minutes < 1440: # < 24 часов
|
||
hours = minutes // 60
|
||
mins = minutes % 60
|
||
if mins > 0:
|
||
return f"{hours}ч {mins}м"
|
||
return f"{hours}ч"
|
||
else: # >= 24 часов
|
||
days = minutes // 1440
|
||
hours = (minutes % 1440) // 60
|
||
if hours > 0:
|
||
return f"{days}д {hours}ч"
|
||
return f"{days}д"
|
||
|
||
|
||
def get_stats_keyboard():
|
||
"""Клавиатура для статистики"""
|
||
ikb = InlineKeyboardBuilder()
|
||
ikb.button(text="🔄 Обновить", callback_data="stats:refresh")
|
||
ikb.button(text="📊 Детали", callback_data="stats:details")
|
||
ikb.button(text="🏆 Топ-спамеры", callback_data="stats:top_spammers")
|
||
ikb.button(text="🔤 Топ-слова", callback_data="stats:top_words")
|
||
ikb.button(text="🚀 Назад", callback_data="start")
|
||
ikb.adjust(2, 2, 1)
|
||
return ikb.as_markup()
|
||
|
||
|
||
# ================= ОСНОВНАЯ СТАТИСТИКА =================
|
||
|
||
@router.callback_query(F.data == "stats:refresh")
|
||
@router.callback_query(F.data == "stats")
|
||
@router.message(Command(*COMMANDS.get("stats", ["stats"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||
@log_action(action_name="VIEW_STATS")
|
||
async def stats_cmd(update: Message | CallbackQuery) -> None:
|
||
"""
|
||
Показывает общую статистику работы бота.
|
||
|
||
Включает:
|
||
- Общее количество удалений
|
||
- Активные режимы
|
||
- Статистику банвордов
|
||
- Топ спамеров
|
||
|
||
Использование: /stats
|
||
"""
|
||
# Определяем тип update
|
||
if isinstance(update, CallbackQuery):
|
||
message = update.message
|
||
is_callback = True
|
||
else:
|
||
message = update
|
||
is_callback = False
|
||
|
||
manager = get_manager()
|
||
|
||
try:
|
||
# Получаем данные
|
||
stats = await manager.get_stats()
|
||
data = await manager.get_all_words_list()
|
||
top_spammers = await manager.get_top_spammers(limit=5)
|
||
|
||
# Проверяем активные режимы
|
||
is_silence = await manager.is_silence_active()
|
||
is_conflict = await manager.is_conflict_active()
|
||
|
||
# === ФОРМИРУЕМ ВЫВОД ===
|
||
|
||
output = "📊 <b>СТАТИСТИКА PRIMOGUARD</b>\n\n"
|
||
|
||
# Общая информация
|
||
total_deletions = stats.get('total_deletions', 0)
|
||
output += f"🗑 <b>Всего удалений:</b> <code>{format_number(total_deletions)}</code>\n\n"
|
||
|
||
# Активные режимы
|
||
if is_silence or is_conflict:
|
||
output += "🔴 <b>АКТИВНЫЕ РЕЖИМЫ:</b>\n\n"
|
||
|
||
if is_silence:
|
||
silence_until_str = await manager.repo.get_setting("silence_until")
|
||
silence_until = datetime.fromtimestamp(float(silence_until_str))
|
||
time_left_seconds = (silence_until - datetime.now()).total_seconds()
|
||
time_left_minutes = int(time_left_seconds / 60)
|
||
|
||
output += f"🔇 <b>Режим тишины</b>\n"
|
||
output += f"├─ ⏱ Осталось: <code>{format_time_remaining(time_left_minutes)}</code>\n"
|
||
output += f"└─ 🕐 До: {format_datetime(silence_until)}\n"
|
||
|
||
if is_conflict:
|
||
output += "│\n"
|
||
|
||
if is_conflict:
|
||
conflict_until_str = await manager.repo.get_setting("conflict_until")
|
||
conflict_until = datetime.fromtimestamp(float(conflict_until_str))
|
||
time_left_seconds = (conflict_until - datetime.now()).total_seconds()
|
||
time_left_minutes = int(time_left_seconds / 60)
|
||
|
||
conflict_words_count = len(data.get('conflict_substring', set()))
|
||
conflict_lemmas_count = len(data.get('conflict_lemma', set()))
|
||
total_conflict = conflict_words_count + conflict_lemmas_count
|
||
|
||
output += f"⚔️ <b>Режим антиконфликта</b>\n"
|
||
output += f"├─ ⏱ Осталось: <code>{format_time_remaining(time_left_minutes)}</code>\n"
|
||
output += f"├─ 🕐 До: {format_datetime(conflict_until)}\n"
|
||
output += f"└─ 📊 Правил: <code>{total_conflict}</code>\n"
|
||
|
||
output += "\n"
|
||
|
||
# Статистика правил
|
||
total_rules = (
|
||
len(data.get('substring', set())) +
|
||
len(data.get('lemma', set())) +
|
||
len(data.get('part', set())) +
|
||
len(data.get('temp_substring', set())) +
|
||
len(data.get('temp_lemma', set())) +
|
||
len(data.get('conflict_substring', set())) +
|
||
len(data.get('conflict_lemma', set()))
|
||
)
|
||
|
||
output += f"📋 <b>Правила модерации:</b>\n"
|
||
output += f"├─ Всего правил: <code>{total_rules}</code>\n"
|
||
output += f"├─ Постоянные: <code>{len(data.get('substring', set())) + len(data.get('lemma', set())) + len(data.get('part', set()))}</code>\n"
|
||
output += f"├─ Временные: <code>{len(data.get('temp_substring', set())) + len(data.get('temp_lemma', set()))}</code>\n"
|
||
output += f"├─ Конфликтные: <code>{len(data.get('conflict_substring', set())) + len(data.get('conflict_lemma', set()))}</code>\n"
|
||
output += f"└─ Исключения: <code>{len(data.get('whitelist', set()))}</code>\n\n"
|
||
|
||
# Топ-5 спамеров
|
||
if top_spammers:
|
||
output += "🏆 <b>Топ-5 спамеров:</b>\n"
|
||
max_count = top_spammers[0][1] if top_spammers else 1
|
||
|
||
for idx, (user_id, count) in enumerate(top_spammers, 1):
|
||
bar = create_text_bar(count, max_count, length=8)
|
||
output += f'{idx}. <a href="tg://user?id={user_id}">{user_id}</a> — {count} [{bar}]\n'
|
||
|
||
output += "\n"
|
||
else:
|
||
output += "🏆 <b>Топ-5 спамеров:</b>\n"
|
||
output += "└─ <i>Нет данных</i>\n\n"
|
||
|
||
# Администраторы
|
||
admins_count = len(settings.OWNER_ID) + len(data.get('admins', set()))
|
||
output += f"👥 <b>Администраторов:</b> <code>{admins_count}</code>\n\n"
|
||
|
||
# Подсказка
|
||
output += "💡 <i>Используйте кнопки для детальной информации</i>"
|
||
|
||
# Клавиатура
|
||
keyboard = get_stats_keyboard()
|
||
|
||
# Отправка
|
||
if is_callback:
|
||
await message.edit_text(
|
||
text=output,
|
||
parse_mode="HTML",
|
||
reply_markup=keyboard
|
||
)
|
||
await update.answer("✅ Статистика обновлена")
|
||
else:
|
||
await message.answer(
|
||
text=output,
|
||
parse_mode="HTML",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка получения статистики: {e}", log_type="STATS")
|
||
|
||
error_text = "❌ <b>Ошибка загрузки статистики</b>\n\nПопробуйте позже"
|
||
|
||
if is_callback:
|
||
await update.answer("❌ Ошибка загрузки", show_alert=True)
|
||
else:
|
||
await message.answer(error_text, parse_mode="HTML")
|
||
|
||
|
||
# ================= ДЕТАЛЬНАЯ СТАТИСТИКА =================
|
||
|
||
@router.callback_query(F.data == "stats:details")
|
||
@log_action(action_name="VIEW_DETAILED_STATS")
|
||
async def stats_details_callback(callback: CallbackQuery) -> None:
|
||
"""Показывает детальную статистику"""
|
||
manager = get_manager()
|
||
|
||
try:
|
||
stats = await manager.get_stats()
|
||
data = await manager.get_all_words_list()
|
||
|
||
output = "📊 <b>ДЕТАЛЬНАЯ СТАТИСТИКА</b>\n\n"
|
||
|
||
# Подробная статистика удалений
|
||
total_deletions = stats.get('total_deletions', 0)
|
||
output += f"🗑 <b>Удаления сообщений:</b>\n"
|
||
output += f"├─ Всего: <code>{format_number(total_deletions)}</code>\n"
|
||
output += "\n"
|
||
|
||
# Активные режимы (детально)
|
||
is_silence = await manager.is_silence_active()
|
||
is_conflict = await manager.is_conflict_active()
|
||
|
||
if is_silence or is_conflict:
|
||
output += "🔴 <b>Активные режимы:</b>\n\n"
|
||
|
||
if is_silence:
|
||
silence_until_str = await manager.repo.get_setting("silence_until")
|
||
silence_until = datetime.fromtimestamp(float(silence_until_str))
|
||
time_left_seconds = (silence_until - datetime.now()).total_seconds()
|
||
time_left_minutes = int(time_left_seconds / 60)
|
||
|
||
output += f"🔇 <b>Режим тишины:</b>\n"
|
||
output += f"├─ Статус: ✅ Активен\n"
|
||
output += f"├─ Осталось: <code>{format_time_remaining(time_left_minutes)}</code>\n"
|
||
output += f"├─ Окончание: {format_datetime(silence_until)}\n"
|
||
output += f"└─ Эффект: Удаляются ВСЕ сообщения\n\n"
|
||
|
||
if is_conflict:
|
||
conflict_until_str = await manager.repo.get_setting("conflict_until")
|
||
conflict_until = datetime.fromtimestamp(float(conflict_until_str))
|
||
time_left_seconds = (conflict_until - datetime.now()).total_seconds()
|
||
time_left_minutes = int(time_left_seconds / 60)
|
||
|
||
conflict_words_count = len(data.get('conflict_substring', set()))
|
||
conflict_lemmas_count = len(data.get('conflict_lemma', set()))
|
||
|
||
output += f"⚔️ <b>Режим антиконфликта:</b>\n"
|
||
output += f"├─ Статус: ✅ Активен\n"
|
||
output += f"├─ Осталось: <code>{format_time_remaining(time_left_minutes)}</code>\n"
|
||
output += f"├─ Окончание: {format_datetime(conflict_until)}\n"
|
||
output += f"├─ Слов: <code>{conflict_words_count}</code>\n"
|
||
output += f"├─ Лемм: <code>{conflict_lemmas_count}</code>\n"
|
||
output += f"└─ Эффект: Обычные банворды отключены\n\n"
|
||
|
||
# Детальная статистика правил
|
||
output += f"📋 <b>Правила модерации:</b>\n\n"
|
||
|
||
output += f"🔴 <b>Постоянные:</b>\n"
|
||
output += f"├─ Подстроки: <code>{len(data.get('substring', set()))}</code>\n"
|
||
output += f"├─ Леммы: <code>{len(data.get('lemma', set()))}</code>\n"
|
||
output += f"└─ Части: <code>{len(data.get('part', set()))}</code>\n\n"
|
||
|
||
output += f"⏱ <b>Временные:</b>\n"
|
||
output += f"├─ Подстроки: <code>{len(data.get('temp_substring', set()))}</code>\n"
|
||
output += f"└─ Леммы: <code>{len(data.get('temp_lemma', set()))}</code>\n\n"
|
||
|
||
output += f"⚔️ <b>Конфликтные:</b>\n"
|
||
output += f"├─ Слова: <code>{len(data.get('conflict_substring', set()))}</code>\n"
|
||
output += f"└─ Леммы: <code>{len(data.get('conflict_lemma', set()))}</code>\n\n"
|
||
|
||
output += f"✅ <b>Исключения:</b> <code>{len(data.get('whitelist', set()))}</code>\n\n"
|
||
|
||
# Информация о кэше
|
||
cache_info = stats.get('cache_active', False)
|
||
cache_updated = stats.get('cache_updated_at', None)
|
||
|
||
output += f"💾 <b>Кэш:</b>\n"
|
||
output += f"├─ Статус: {'✅ Активен' if cache_info else '❌ Неактивен'}\n"
|
||
|
||
if cache_updated and isinstance(cache_updated, str):
|
||
try:
|
||
updated_dt = datetime.fromisoformat(cache_updated)
|
||
output += f"└─ Обновлён: {format_datetime(updated_dt)}\n"
|
||
except (ValueError, TypeError):
|
||
output += f"└─ Обновлён: недавно\n"
|
||
else:
|
||
output += f"└─ Не обновлялся\n"
|
||
|
||
# Кнопка возврата
|
||
ikb = InlineKeyboardBuilder()
|
||
ikb.button(text="◀️ Назад", callback_data="stats:refresh")
|
||
|
||
await callback.message.edit_text(
|
||
text=output,
|
||
parse_mode="HTML",
|
||
reply_markup=ikb.as_markup()
|
||
)
|
||
await callback.answer()
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка получения детальной статистики: {e}", log_type="STATS")
|
||
await callback.answer("❌ Ошибка загрузки", show_alert=True)
|
||
|
||
|
||
# ================= ТОП СПАМЕРОВ =================
|
||
|
||
@router.callback_query(F.data == "stats:top_spammers")
|
||
@log_action(action_name="VIEW_TOP_SPAMMERS")
|
||
async def stats_top_spammers_callback(callback: CallbackQuery) -> None:
|
||
"""Показывает топ-10 спамеров"""
|
||
manager = get_manager()
|
||
|
||
try:
|
||
top_spammers = await manager.get_top_spammers(limit=10)
|
||
|
||
output = "🏆 <b>ТОП-10 СПАМЕРОВ</b>\n\n"
|
||
|
||
if top_spammers:
|
||
max_count = top_spammers[0][1] if top_spammers else 1
|
||
|
||
for idx, (user_id, count) in enumerate(top_spammers, 1):
|
||
bar = create_text_bar(count, max_count, length=10)
|
||
|
||
# Эмодзи для топ-3
|
||
if idx == 1:
|
||
medal = "🥇"
|
||
elif idx == 2:
|
||
medal = "🥈"
|
||
elif idx == 3:
|
||
medal = "🥉"
|
||
else:
|
||
medal = f"{idx}."
|
||
|
||
output += f'{medal} <a href="tg://user?id={user_id}">{user_id}</a>\n'
|
||
output += f" └─ {format_number(count)} удалений [{bar}]\n\n"
|
||
|
||
# Общая статистика
|
||
total_spammers = len(top_spammers)
|
||
total_deletions = sum(count for _, count in top_spammers)
|
||
|
||
output += f"📊 <b>Статистика:</b>\n"
|
||
output += f"├─ Всего пользователей: <code>{total_spammers}</code>\n"
|
||
output += f"└─ Всего удалений: <code>{format_number(total_deletions)}</code>\n\n"
|
||
|
||
output += "💡 <i>ID можно использовать для проверки пользователя</i>"
|
||
else:
|
||
output += "└─ <i>Нет данных об удалениях</i>\n\n"
|
||
output += "💡 <i>Когда бот начнёт удалять сообщения, здесь появится статистика</i>"
|
||
|
||
# Кнопка возврата
|
||
ikb = InlineKeyboardBuilder()
|
||
ikb.button(text="◀️ Назад", callback_data="stats:refresh")
|
||
|
||
await callback.message.edit_text(
|
||
text=output,
|
||
parse_mode="HTML",
|
||
reply_markup=ikb.as_markup()
|
||
)
|
||
await callback.answer()
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка получения топ спамеров: {e}", log_type="STATS")
|
||
await callback.answer("❌ Ошибка загрузки", show_alert=True)
|
||
|
||
|
||
# ================= ТОП СЛОВ =================
|
||
|
||
@router.callback_query(F.data == "stats:top_words")
|
||
async def stats_top_words_callback(callback: CallbackQuery) -> None:
|
||
"""Показывает топ-10 самых частых срабатываний"""
|
||
await callback.answer()
|
||
|
||
manager = get_manager()
|
||
|
||
# Получаем топ слов
|
||
top_words = await manager.get_top_words(limit=10)
|
||
|
||
if not top_words:
|
||
text = (
|
||
"🔤 <b>ТОП-10 СРАБАТЫВАНИЙ ПО СЛОВАМ</b>\n\n"
|
||
"📭 <i>Статистика пока пуста</i>\n\n"
|
||
"Срабатывания появятся после удаления\n"
|
||
"первых спам-сообщений."
|
||
)
|
||
else:
|
||
text = "🔤 <b>ТОП-10 СРАБАТЫВАНИЙ ПО СЛОВАМ</b>\n\n"
|
||
|
||
# Эмодзи для типов
|
||
type_emoji = {
|
||
"substring": "🔤",
|
||
"lemma": "📖",
|
||
"part": "🧩",
|
||
"silence": "🔇",
|
||
"conflict_substring": "⚔️",
|
||
"conflict_lemma": "⚔️"
|
||
}
|
||
|
||
for i, word_data in enumerate(top_words, 1):
|
||
word = word_data['word']
|
||
count = word_data['count']
|
||
word_type = word_data['type']
|
||
emoji = type_emoji.get(word_type, "❓")
|
||
|
||
# Медали для топ-3
|
||
medal = ""
|
||
if i == 1:
|
||
medal = "🥇 "
|
||
elif i == 2:
|
||
medal = "🥈 "
|
||
elif i == 3:
|
||
medal = "🥉 "
|
||
|
||
text += f"{medal}<b>{i}.</b> {emoji} <code>{word}</code> — {count} раз\n"
|
||
|
||
# Общая статистика
|
||
total = await manager.get_total_spam_count()
|
||
text += f"\n📊 <b>Всего удалено:</b> {total} сообщений"
|
||
|
||
# Кнопка назад
|
||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||
[InlineKeyboardButton(text="◀️ Назад", callback_data="show_stats")]
|
||
])
|
||
|
||
try:
|
||
await callback.message.edit_text(
|
||
text=text,
|
||
reply_markup=keyboard,
|
||
parse_mode="HTML"
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка показа топ-слов: {e}", log_type="ERROR")
|
||
await callback.answer("❌ Ошибка загрузки статистики", show_alert=True)
|
||
|
||
|
||
# ================= СТАТИСТИКА ПОЛЬЗОВАТЕЛЯ =================
|
||
|
||
@router.message(Command(*COMMANDS.get("userstats", ["userstats"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||
@log_action(action_name="VIEW_USER_STATS", log_args=True)
|
||
async def user_stats_cmd(message: Message) -> None:
|
||
"""
|
||
Показывает статистику конкретного пользователя.
|
||
|
||
Использование: /userstats <ID>
|
||
Пример: /userstats 123456789
|
||
"""
|
||
parts = message.text.split(maxsplit=1)
|
||
|
||
if len(parts) < 2:
|
||
await message.answer(
|
||
"❌ Использование: <code>/userstats [ID]</code>\n\n"
|
||
"Пример: <code>/userstats 123456789</code>",
|
||
parse_mode="HTML"
|
||
)
|
||
return
|
||
|
||
try:
|
||
user_id = int(parts[1].strip())
|
||
except ValueError:
|
||
await message.answer("❌ ID должен быть числом", parse_mode="HTML")
|
||
return
|
||
|
||
manager = get_manager()
|
||
|
||
try:
|
||
# Получаем статистику пользователя
|
||
user_spam_count = await manager.get_user_spam_count(user_id)
|
||
user_spam_stats = await manager.get_spam_stats(limit=10, user_id=user_id)
|
||
|
||
output = f"👤 <b>СТАТИСТИКА ПОЛЬЗОВАТЕЛЯ</b>\n\n"
|
||
output += f'🆔 ID: <a href="tg://user?id={user_id}">{user_id}</a>\n\n'
|
||
|
||
if user_spam_count > 0:
|
||
output += f"🗑 <b>Удалено сообщений:</b> <code>{format_number(user_spam_count)}</code>\n\n"
|
||
|
||
if user_spam_stats:
|
||
output += f"📝 <b>Последние удаления:</b>\n"
|
||
|
||
for stat in user_spam_stats[:5]:
|
||
deleted_at = stat.deleted_at
|
||
matched_word = stat.matched_word or "неизвестно"
|
||
match_type = stat.match_type or "unknown"
|
||
|
||
output += f"├─ {format_datetime(deleted_at)}\n"
|
||
output += f"│ └─ Слово: <code>{matched_word}</code> ({match_type})\n"
|
||
|
||
output += "\n"
|
||
else:
|
||
output += "✅ <i>Нет нарушений</i>\n\n"
|
||
output += "Этот пользователь не нарушал правила чата"
|
||
|
||
await message.answer(output, parse_mode="HTML")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка получения статистики пользователя: {e}", log_type="STATS")
|
||
await message.answer("❌ <b>Ошибка загрузки статистики</b>", parse_mode="HTML")
|
||
|
||
|
||
# ================= СБРОС СТАТИСТИКИ =================
|
||
|
||
@router.message(Command(*COMMANDS.get("resetstats", ["resetstats"]), prefix=settings.PREFIX, ignore_case=True),
|
||
IsAdmin())
|
||
@log_action(action_name="RESET_STATS")
|
||
async def reset_stats_cmd(message: Message) -> None:
|
||
"""
|
||
Сбрасывает всю статистику удалений.
|
||
|
||
⚠️ ВНИМАНИЕ: Это действие необратимо!
|
||
|
||
Использование: /resetstats confirm
|
||
"""
|
||
parts = message.text.split(maxsplit=1)
|
||
|
||
if len(parts) < 2 or parts[1].lower() != "confirm":
|
||
await message.answer(
|
||
"⚠️ <b>ВНИМАНИЕ!</b>\n\n"
|
||
"Эта команда удалит ВСЮ статистику удалений:\n"
|
||
"• Счётчики удалений пользователей\n"
|
||
"• Историю удалённых сообщений\n"
|
||
"• Топ спамеров\n\n"
|
||
"Правила модерации НЕ будут удалены.\n\n"
|
||
"Для подтверждения используйте:\n"
|
||
"<code>/resetstats confirm</code>",
|
||
parse_mode="HTML"
|
||
)
|
||
return
|
||
|
||
manager = get_manager()
|
||
|
||
try:
|
||
# Сбрасываем статистику
|
||
deleted_count = await manager.reset_spam_stats()
|
||
|
||
if deleted_count > 0:
|
||
await message.answer(
|
||
f"✅ <b>Статистика сброшена</b>\n\n"
|
||
f"Удалено записей: {deleted_count}\n\n"
|
||
f"Новые данные начнут собираться\n"
|
||
f"с этого момента.",
|
||
parse_mode="HTML"
|
||
)
|
||
logger.warning(
|
||
f"Статистика сброшена пользователем {message.from_user.id}: "
|
||
f"удалено {deleted_count} записей",
|
||
log_type="STATS"
|
||
)
|
||
else:
|
||
await message.answer(
|
||
"ℹ️ <b>Статистика уже пуста</b>",
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка сброса статистики: {e}", log_type="STATS")
|
||
await message.answer("❌ <b>Ошибка сброса статистики</b>", parse_mode="HTML")
|
||
|