Получение статистики по блокировкам
This commit is contained in:
589
bot/handlers/commands/users/stats.py
Normal file
589
bot/handlers/commands/users/stats.py
Normal file
@@ -0,0 +1,589 @@
|
|||||||
|
"""
|
||||||
|
Обработчики команды статистики
|
||||||
|
"""
|
||||||
|
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="stats:refresh")]
|
||||||
|
])
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
Reference in New Issue
Block a user