🐛 ИСПРАВЛЕНИЯ: • chat_monitor.py: message.system → hasattr проверка • База данных: добавлена колонка achievement_data • Удалена кнопка /settings из всех шаблонов • Настройки перенесены в /broadcast 📁 ФАЙЛЫ: • handlers/chat_monitor.py • web/templates/*.html (удалена /settings) • database: ALTER TABLE achievements Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
389 lines
19 KiB
Python
389 lines
19 KiB
Python
"""
|
||
Мониторинг чата
|
||
Считывает все сообщения для статистики и детекта шпионов
|
||
"""
|
||
import logging
|
||
from datetime import datetime
|
||
from aiogram import Router, F
|
||
from aiogram.types import Message
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select
|
||
from database.db import AsyncSessionLocal
|
||
from database.models import User, Message as MessageModel, SpamWarning
|
||
from services.spy_detector import SpyDetector
|
||
from services.antispam import AntiSpamService
|
||
import config
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = Router()
|
||
|
||
# Баллы за активность
|
||
RATING_PER_MESSAGE = 1 # +1 балл за каждое сообщение
|
||
|
||
|
||
@router.message()
|
||
async def monitor_all_messages(message: Message):
|
||
"""
|
||
Перехват всех сообщений в чате
|
||
Для статистики и анализа
|
||
"""
|
||
# Пропускаем сообщения от ботов
|
||
if message.from_user.is_bot:
|
||
return
|
||
|
||
# Пропускаем сервисные сообщения (в aiogram 3.x нет message.system)
|
||
if message.chat.type != 'private' and hasattr(message, 'is_topic_closed'):
|
||
# Это сообщение из темы форума — пропускаем служебные
|
||
pass
|
||
|
||
user_id = message.from_user.id
|
||
chat_id = message.chat.id
|
||
text = message.text or ''
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
# Получаем или создаём пользователя
|
||
stmt = select(User).where(User.user_id == user_id)
|
||
result = await session.execute(stmt)
|
||
user = result.scalar_one_or_none()
|
||
|
||
if not user:
|
||
# Создаём нового пользователя
|
||
user = User(
|
||
user_id=user_id,
|
||
username=message.from_user.username,
|
||
first_name=message.from_user.first_name,
|
||
last_name=message.from_user.last_name,
|
||
)
|
||
session.add(user)
|
||
else:
|
||
# Логируем изменения профиля
|
||
from database.models import ProfileHistory
|
||
|
||
changes = []
|
||
|
||
if user.username != message.from_user.username:
|
||
changes.append(('username', user.username, message.from_user.username))
|
||
user.username = message.from_user.username
|
||
|
||
if user.first_name != message.from_user.first_name:
|
||
changes.append(('first_name', user.first_name, message.from_user.first_name))
|
||
user.first_name = message.from_user.first_name
|
||
|
||
if user.last_name != message.from_user.last_name:
|
||
changes.append(('last_name', user.last_name, message.from_user.last_name))
|
||
user.last_name = message.from_user.last_name
|
||
|
||
# Сохраняем историю изменений
|
||
for field_name, old_value, new_value in changes:
|
||
history_record = ProfileHistory(
|
||
user_id=user_id,
|
||
field_name=field_name,
|
||
old_value=old_value,
|
||
new_value=new_value,
|
||
)
|
||
session.add(history_record)
|
||
|
||
if old_value:
|
||
logger.info(f'Profile change: user_id={user_id}, {field_name}: {old_value} → {new_value}')
|
||
|
||
# Обновляем данные
|
||
user.message_count += 1
|
||
user.last_seen = datetime.utcnow()
|
||
|
||
# Начисляем рейтинг за сообщение
|
||
user.rating += RATING_PER_MESSAGE
|
||
user.update_level() # Обновляем уровень
|
||
|
||
# ==========================================
|
||
# ПРОВЕРКА ДОСТИЖЕНИЙ
|
||
# ==========================================
|
||
if not config.is_admin(user_id):
|
||
from services.achievements import AchievementsService
|
||
achievements_service = AchievementsService(session)
|
||
|
||
# Проверяем и выдаём достижения
|
||
awarded = await achievements_service.check_and_award(
|
||
user_id,
|
||
message_count=user.message_count,
|
||
rating=user.rating,
|
||
message_hour=datetime.utcnow().hour
|
||
)
|
||
|
||
# Уведомляем о полученных достижениях
|
||
for ach_type in awarded:
|
||
ach_info = achievements_service.get_achievement_info(ach_type)
|
||
try:
|
||
await message.bot.send_message(
|
||
user_id,
|
||
f'🏆 <b>Новое достижение!</b>\n\n'
|
||
f'{ach_info.get("emoji", "🏆")} <b>{ach_info.get("name", ach_type)}</b>\n\n'
|
||
f'{ach_info.get("description", "")}\n\n'
|
||
f'Продолжайте в том же духе!',
|
||
parse_mode='HTML'
|
||
)
|
||
logger.info(f'Пользователь {user_id} получил достижение {ach_type}')
|
||
except Exception as e:
|
||
logger.error(f'Не удалось уведомить о достижении: {e}')
|
||
|
||
# ==========================================
|
||
# ПРОВЕРКА НА СПАМ (чёрный список ссылок)
|
||
# ==========================================
|
||
if text and not config.is_admin(user_id):
|
||
antispam = AntiSpamService(session)
|
||
is_spam, found_items, reason = antispam.check_message(text)
|
||
settings = antispam.get_settings()
|
||
|
||
if is_spam:
|
||
# Сохраняем предупреждение
|
||
spam_warning = SpamWarning(
|
||
user_id=user_id,
|
||
message_text=text[:1000] if text else None,
|
||
reason=reason,
|
||
)
|
||
session.add(spam_warning)
|
||
|
||
# Увеличиваем счётчик предупреждений
|
||
user.spam_warning_count += 1
|
||
warning_count = user.spam_warning_count
|
||
|
||
# Получаем настройки
|
||
delete_message = settings.get('delete_message', True)
|
||
warn_user = settings.get('warn_user', True)
|
||
notify_admin = settings.get('notify_admin', True)
|
||
ban_after = settings.get('ban_after_warnings', 3)
|
||
|
||
# Удаляем сообщение если нужно
|
||
if delete_message:
|
||
try:
|
||
await message.delete()
|
||
logger.info(f'Спам удалён от user_id={user_id}: {reason}')
|
||
except Exception as e:
|
||
logger.error(f'Не удалось удалить спам: {e}')
|
||
|
||
# Предупреждаем пользователя
|
||
if warn_user:
|
||
try:
|
||
await message.bot.send_message(
|
||
user_id,
|
||
f'⚠️ <b>Предупреждение #{warning_count}</b>\n\n'
|
||
f'В вашем сообщении обнаружена запрещённая ссылка или спам.\n\n'
|
||
f'Причина: {reason}\n\n'
|
||
f'📊 Ваши нарушения: {warning_count} из {ban_after}\n\n'
|
||
f'При следующем нарушении будет публичное предупреждение.\n'
|
||
f'При достижении {ban_after} предупреждений — бан.',
|
||
parse_mode='HTML'
|
||
)
|
||
except Exception as e:
|
||
logger.error(f'Не удалось предупредить пользователя: {e}')
|
||
|
||
# Уведомляем админа
|
||
if notify_admin:
|
||
try:
|
||
await message.bot.send_message(
|
||
config.ADMIN_USER_ID,
|
||
f'⚠️ <b>СПАМ В ЧАТЕ!</b>\n\n'
|
||
f'Пользователь: {user.full_name}\n'
|
||
f'ID: <code>{user.user_id}</code>\n'
|
||
f'Квартира: {user.apartment or "не указана"}\n\n'
|
||
f'Причина: {reason}\n\n'
|
||
f'Предупреждений: {warning_count} из {ban_after}',
|
||
parse_mode='HTML'
|
||
)
|
||
except Exception as e:
|
||
logger.error(f'Не удалось уведомить админа о спаме: {e}')
|
||
|
||
# Бан после достижения лимита
|
||
if warning_count >= ban_after:
|
||
user.is_banned = True
|
||
try:
|
||
await message.bot.send_message(
|
||
user_id,
|
||
f'🚫 <b>ВЫ ЗАБАНЕНЫ</b>\n\n'
|
||
f'Причина: Систематический спам ({warning_count} предупреждений).\n\n'
|
||
f'Вы больше не можете писать в чат.',
|
||
parse_mode='HTML'
|
||
)
|
||
# Пытаемся удалить из чата
|
||
await message.bot.ban_chat_member(chat_id, user_id)
|
||
except Exception as e:
|
||
logger.error(f'Не удалось забанить спамера: {e}')
|
||
|
||
logger.warning(f'Пользователь {user_id} забанен за спам')
|
||
|
||
await session.commit()
|
||
|
||
# Прерываем дальнейшую обработку если спам
|
||
return
|
||
|
||
# ==========================================
|
||
# ПРОВЕРКА ЛИМИТОВ СООБЩЕНИЙ (анти-спам)
|
||
# ==========================================
|
||
if not config.is_admin(user_id):
|
||
from services.rate_limiter import RateLimiter
|
||
rate_limiter = RateLimiter(session)
|
||
|
||
# Проверяем не замучен ли пользователь
|
||
if user.is_muted and user.mute_until:
|
||
# Проверяем истёк ли мут
|
||
if await rate_limiter.check_mute_expired(user_id):
|
||
# Мут истёк — пропускаем сообщение
|
||
pass
|
||
else:
|
||
# Ещё замучен — удаляем сообщение
|
||
try:
|
||
await message.delete()
|
||
logger.info(f'Сообщение от замученного user_id={user_id} удалено')
|
||
except Exception as e:
|
||
logger.error(f'Не удалось удалить сообщение замученного: {e}')
|
||
return
|
||
|
||
# Проверяем лимиты
|
||
is_limited, msg_count = await rate_limiter.check_rate_limit(user_id)
|
||
|
||
if is_limited:
|
||
# Добавляем сообщение в счётчик (для статистики)
|
||
await rate_limiter.add_message(user_id)
|
||
|
||
# Увеличиваем предупреждения
|
||
user.rate_limit_warnings += 1
|
||
warning_count = user.rate_limit_warnings
|
||
|
||
config_rl = rate_limiter.get_config()
|
||
mute_after = config_rl.get('warnings_before_mute', 2)
|
||
|
||
# Удаляем сообщение
|
||
try:
|
||
await message.delete()
|
||
logger.info(f'Флуд удалён от user_id={user_id}')
|
||
except Exception as e:
|
||
logger.error(f'Не удалось удалить флуд: {e}')
|
||
|
||
# Предупреждаем пользователя
|
||
try:
|
||
await message.bot.send_message(
|
||
user_id,
|
||
f'⚠️ <b>Предупреждение #{warning_count}</b>\n\n'
|
||
f'Вы превысили лимит сообщений!\n\n'
|
||
f'📊 Ваши нарушения: {warning_count} из {mute_after}\n\n'
|
||
f'Лимит: {config_rl["max_messages"]} сообщений за {config_rl["period_seconds"]} сек.\n'
|
||
f'Вы отправили: {msg_count} сообщ.\n\n'
|
||
f'При достижении {mute_after} предупреждений вы будете замучены на {config_rl["mute_duration_minutes"]} мин.',
|
||
parse_mode='HTML'
|
||
)
|
||
except Exception as e:
|
||
logger.error(f'Не удалось предупредить о флуде: {e}')
|
||
|
||
# Уведомляем админа
|
||
try:
|
||
await message.bot.send_message(
|
||
config.ADMIN_USER_ID,
|
||
f'⚠️ <b>ФЛУД В ЧАТЕ!</b>\n\n'
|
||
f'Пользователь: {user.full_name}\n'
|
||
f'ID: <code>{user.user_id}</code>\n'
|
||
f'Квартира: {user.apartment or "не указана"}\n\n'
|
||
f'Сообщений: {msg_count} (лимит: {config_rl["max_messages"]})\n'
|
||
f'Предупреждений: {warning_count} из {mute_after}',
|
||
parse_mode='HTML'
|
||
)
|
||
except Exception as e:
|
||
logger.error(f'Не удалось уведомить админа о флуде: {e}')
|
||
|
||
# Мут после достижения лимита предупреждений
|
||
if warning_count >= mute_after:
|
||
mute_until = await rate_limiter.mute_user(user_id)
|
||
|
||
try:
|
||
await message.bot.send_message(
|
||
user_id,
|
||
f'🔇 <b>ВЫ ЗАМУЧЕНЫ</b>\n\n'
|
||
f'Причина: Превышение лимита сообщений ({warning_count} предупреждений).\n\n'
|
||
f'Мут до: {mute_until.strftime("%d.%m.%Y %H:%M")}\n\n'
|
||
f'Вы не сможете писать в чат до указанного времени.',
|
||
parse_mode='HTML'
|
||
)
|
||
except Exception as e:
|
||
logger.error(f'Не удалось уведомить о муте: {e}')
|
||
|
||
logger.warning(f'Пользователь {user_id} замучен за флуд')
|
||
|
||
await session.commit()
|
||
return
|
||
|
||
# Добавляем сообщение в счётчик (если не превышен лимит)
|
||
await rate_limiter.add_message(user_id)
|
||
|
||
# Классифицируем тему сообщения
|
||
topic = None
|
||
if text:
|
||
from services.topics_analyzer import TopicsAnalyzer
|
||
topics_analyzer = TopicsAnalyzer(session)
|
||
topic = await topics_analyzer.analyze_message(text)
|
||
|
||
# Анализируем токсичность сообщения
|
||
toxicity_score = 0
|
||
if text and not config.is_admin(user_id):
|
||
from services.toxicity_detector import ToxicityDetector
|
||
toxicity_detector = ToxicityDetector(session)
|
||
toxicity_score, _ = await toxicity_detector.analyze_message(text)
|
||
|
||
# Обновляем статистику пользователя
|
||
if toxicity_score > 0:
|
||
user.toxic_messages_count += 1
|
||
# Обновляем средний score
|
||
total_messages = user.message_count + 1
|
||
user.toxicity_score = int(
|
||
(user.toxicity_score * (total_messages - 1) + toxicity_score) / total_messages
|
||
)
|
||
|
||
# Уведомляем админа если токсичность высокая
|
||
if toxicity_score > 50:
|
||
try:
|
||
await message.bot.send_message(
|
||
config.ADMIN_USER_ID,
|
||
f'⚠️ <b>Токсичное сообщение!</b>\n\n'
|
||
f'Пользователь: {user.full_name}\n'
|
||
f'ID: <code>{user.user_id}</code>\n'
|
||
f'Квартира: {user.apartment or "не указана"}\n'
|
||
f'Toxicity Score: {toxicity_score}/100\n\n'
|
||
f'Проверьте через /toxic_stats {user.user_id}',
|
||
parse_mode='HTML'
|
||
)
|
||
except Exception as e:
|
||
logger.error(f'Не удалось уведомить о токсичности: {e}')
|
||
|
||
# Сохраняем сообщение (если не спам)
|
||
msg_model = MessageModel(
|
||
message_id=message.message_id,
|
||
user_id=user_id,
|
||
chat_id=chat_id,
|
||
text=text[:4000] if text else None,
|
||
topic=topic, # Сохраняем тему
|
||
reply_to_message_id=message.reply_to_message.message_id if message.reply_to_message else None,
|
||
)
|
||
session.add(msg_model)
|
||
|
||
await session.commit()
|
||
|
||
# Проверяем на шпиона (только если не админ)
|
||
if not config.is_admin(user_id) and config.SPY_DETECTION_ENABLED:
|
||
spy_detector = SpyDetector(session)
|
||
changed = await spy_detector.update_spy_score(user)
|
||
|
||
# Если score стал критическим — уведомляем админа
|
||
if user.spy_score > 60 and changed:
|
||
try:
|
||
await message.bot.send_message(
|
||
config.get_admin_chat_id(),
|
||
f'🚨 <b>ВНИМАНИЕ! ВОЗМОЖНЫЙ ШПИОН!</b>\n\n'
|
||
f'Пользователь: {user.full_name}\n'
|
||
f'ID: <code>{user.user_id}</code>\n'
|
||
f'Квартира: {user.apartment or "не указана"}\n'
|
||
f'Spy Score: {user.spy_score}/100\n\n'
|
||
f'Флаги: {user.spy_flags}',
|
||
parse_mode='HTML'
|
||
)
|
||
except Exception as e:
|
||
logger.error(f'Не удалось уведомить о шпионе: {e}')
|
||
|
||
logger.debug(f"User {user_id}: msg_count={user.message_count}, rating={user.rating}, level={user.level}")
|