""" Multicount Detector — детектор мультиаккаунтов Анализ паттернов поведения для выявления нескольких аккаунтов от одного человека """ import logging from datetime import datetime, timedelta from typing import Dict, List, Tuple, Optional from collections import defaultdict from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession logger = logging.getLogger(__name__) class MulticountDetector: """Детектор мультиаккаунтов""" def __init__(self, session: AsyncSession): self.session = session self.config = self._get_default_config() def _get_default_config(self) -> Dict: """Конфигурация по умолчанию""" return { 'time_window_hours': 24, # Окно анализа (часов) 'activity_similarity_threshold': 0.8, # Порог схожести активности 'message_pattern_threshold': 0.7, # Порог схожести паттернов сообщений 'same_device_score': 50, # Баллы за same device 'same_activity_score': 30, # Баллы за схожую активность 'same_pattern_score': 20, # Баллы за схожие паттерны } async def analyze_all_users(self) -> List[Dict]: """ Проанализировать всех пользователей на мультиаккаунты Returns: список пар/групп подозрительных аккаунтов """ from database.models import User # Получаем всех активных пользователей stmt = select(User).where(User.is_banned == False) result = await self.session.execute(stmt) users = list(result.scalars().all()) if len(users) < 2: return [] suspicious_pairs = [] # Сравниваем каждого с каждым for i in range(len(users)): for j in range(i + 1, len(users)): user1 = users[i] user2 = users[j] # Пропускаем если уже в одной квартире if user1.apartment and user2.apartment: if user1.apartment == user2.apartment: continue score, reasons = await self._compare_users(user1, user2) if score >= 50: # Порог подозрительности suspicious_pairs.append({ 'user1_id': user1.user_id, 'user1_name': user1.full_name, 'user1_apartment': user1.apartment, 'user2_id': user2.user_id, 'user2_name': user2.full_name, 'user2_apartment': user2.apartment, 'score': score, 'reasons': reasons, }) return suspicious_pairs async def _compare_users(self, user1, user2) -> Tuple[int, List[str]]: """ Сравнить двух пользователей Returns: (score, reasons) """ score = 0 reasons = [] # 1. Проверка активности по времени activity_similarity = await self._check_activity_similarity(user1.user_id, user2.user_id) if activity_similarity >= self.config['activity_similarity_threshold']: score += self.config['same_activity_score'] reasons.append(f'Схожая активность ({activity_similarity:.0%})') # 2. Проверка паттернов сообщений pattern_similarity = await self._check_message_patterns(user1.user_id, user2.user_id) if pattern_similarity >= self.config['message_pattern_threshold']: score += self.config['same_pattern_score'] reasons.append(f'Схожие паттерны сообщений ({pattern_similarity:.0%})') # 3. Проверка device ID (если доступно) # Telegram не отдаёт device ID через Bot API, но можно проверять по другим признакам # 4. Проверка времени регистрации if user1.join_date and user2.join_date: delta = abs((user1.join_date - user2.join_date).total_seconds()) if delta < 3600: # Зарегистрировались в течение часа score += 10 reasons.append('Зарегистрировались в течение часа') # 5. Проверка IP (если бы было доступно) # В Telegram ботах IP недоступен return score, reasons async def _check_activity_similarity(self, user1_id: int, user2_id: int) -> float: """ Проверить схожесть активности по времени Returns: коэффициент схожести 0-1 """ from database.models import Message # Получаем временное окно time_window = datetime.utcnow() - timedelta(hours=self.config['time_window_hours']) # Активность по часам для user1 stmt = select(func.strftime('%H', Message.timestamp)).where( Message.user_id == user1_id, Message.timestamp >= time_window ) result = await self.session.execute(stmt) user1_hours = [row[0] for row in result.all()] # Активность по часам для user2 stmt = select(func.strftime('%H', Message.timestamp)).where( Message.user_id == user2_id, Message.timestamp >= time_window ) result = await self.session.execute(stmt) user2_hours = [row[0] for row in result.all()] if not user1_hours or not user2_hours: return 0.0 # Считаем активность по часам user1_activity = defaultdict(int) user2_activity = defaultdict(int) for hour in user1_hours: user1_activity[hour] += 1 for hour in user2_hours: user2_activity[hour] += 1 # Сравниваем распределение common_hours = set(user1_activity.keys()) & set(user2_activity.keys()) if not common_hours: return 0.0 # Коэффициент схожести similarity = len(common_hours) / 24 # Доля общих часов активности # Дополнительно проверяем пики активности user1_peak = max(user1_activity.keys(), key=lambda h: user1_activity[h]) if user1_activity else None user2_peak = max(user2_activity.keys(), key=lambda h: user2_activity[h]) if user2_activity else None if user1_peak and user2_peak and abs(int(user1_peak) - int(user2_peak)) <= 1: similarity += 0.2 # Бонус за схожий пик активности return min(similarity, 1.0) async def _check_message_patterns(self, user1_id: int, user2_id: int) -> float: """ Проверить схожесть паттернов сообщений Returns: коэффициент схожести 0-1 """ from database.models import Message # Получаем последние сообщения time_window = datetime.utcnow() - timedelta(hours=self.config['time_window_hours']) stmt = select(Message.text).where( Message.user_id == user1_id, Message.timestamp >= time_window, Message.text.isnot(None) ).limit(50) result = await self.session.execute(stmt) user1_messages = [row[0] for row in result.all()] stmt = select(Message.text).where( Message.user_id == user2_id, Message.timestamp >= time_window, Message.text.isnot(None) ).limit(50) result = await self.session.execute(stmt) user2_messages = [row[0] for row in result.all()] if not user1_messages or not user2_messages: return 0.0 # Анализируем паттерны user1_patterns = self._extract_patterns(user1_messages) user2_patterns = self._extract_patterns(user2_messages) # Сравниваем паттерны common_patterns = set(user1_patterns.keys()) & set(user2_patterns.keys()) if not common_patterns: return 0.0 # Коэффициент схожести паттернов similarity = len(common_patterns) / max(len(user1_patterns), len(user2_patterns)) return min(similarity, 1.0) def _extract_patterns(self, messages: List[str]) -> Dict: """ Извлечь паттерны из сообщений Returns: dict с паттернами """ patterns = defaultdict(int) for msg in messages: msg = msg.lower().strip() # Длина сообщения if len(msg) < 10: patterns['short'] += 1 elif len(msg) < 50: patterns['medium'] += 1 else: patterns['long'] += 1 # Наличие ссылок if 'http' in msg or 'www' in msg or 't.me' in msg: patterns['with_links'] += 1 # Наличие emoji if any(ord(c) > 12700 for c in msg): patterns['with_emoji'] += 1 # Вопросы if '?' in msg: patterns['questions'] += 1 # Приветствия if msg.startswith(('привет', 'здравствуй', 'добрый')): patterns['greetings'] += 1 # Мат (уже есть в antimat) # Можно добавить проверку return patterns async def check_apartment(self, apartment: str) -> List[Dict]: """ Проверить квартиру на мультиаккаунты Returns: список аккаунтов в квартире """ from database.models import User stmt = select(User).where( User.apartment == apartment, User.is_banned == False ) result = await self.session.execute(stmt) users = list(result.scalars().all()) if len(users) <= 1: return [] # Проверяем пары на подозрительность suspicious = [] for i in range(len(users)): for j in range(i + 1, len(users)): score, reasons = await self._compare_users(users[i], users[j]) if score >= 50: suspicious.append({ 'user1': {'id': users[i].user_id, 'name': users[i].full_name}, 'user2': {'id': users[j].user_id, 'name': users[j].full_name}, 'score': score, 'reasons': reasons, }) return suspicious def get_config(self) -> Dict: """Получить конфигурацию""" return self.config