domovoy_bot/handlers/chat_monitor.py
Admin ba4676b0af v1.0 MVP - Инициализация проекта 🏠
 Регистрация и верификация жильцов
 Рейтинг активности (уровни)
 Детект шпионов (Score 0-100)
 Анти-мат система (3 предупреждения → бан)
 Админ-панель с рассылкой
 Учёт квартир (несколько жильцов)
 Телефоны экстренных служб и мастеров
 Еженедельный экспорт JSON
 Прокси (socks5) для обхода РКН

Бекап: backups/versions/v1.0_mvp_2026-02-26/

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-27 10:54:16 +00:00

388 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Мониторинг чата
Считывает все сообщения для статистики и детекта шпионов
"""
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
# Пропускаем сервисные сообщения
if message.system:
return
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}")