✅ Регистрация и верификация жильцов ✅ Рейтинг активности (уровни) ✅ Детект шпионов (Score 0-100) ✅ Анти-мат система (3 предупреждения → бан) ✅ Админ-панель с рассылкой ✅ Учёт квартир (несколько жильцов) ✅ Телефоны экстренных служб и мастеров ✅ Еженедельный экспорт JSON ✅ Прокси (socks5) для обхода РКН Бекап: backups/versions/v1.0_mvp_2026-02-26/ Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
205 lines
7.8 KiB
Python
205 lines
7.8 KiB
Python
"""
|
||
Детектор шпионов - анализ активности пользователей
|
||
"""
|
||
import json
|
||
import logging
|
||
from datetime import datetime, timedelta
|
||
from typing import List, Dict, Tuple
|
||
from sqlalchemy import select, func
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from database.models import User, Message, SpyLog
|
||
from config import ADMIN_USER_ID
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class SpyDetector:
|
||
"""Детектор подозрительной активности"""
|
||
|
||
def __init__(self, session: AsyncSession):
|
||
self.session = session
|
||
self.patterns = self._load_patterns()
|
||
|
||
def _load_patterns(self) -> Dict:
|
||
"""Загрузка паттернов из JSON"""
|
||
try:
|
||
with open('data/spies_patterns.json', 'r', encoding='utf-8') as f:
|
||
return json.load(f)
|
||
except Exception as e:
|
||
logger.error(f'Не удалось загрузить паттерны: {e}')
|
||
return self._get_default_patterns()
|
||
|
||
def _get_default_patterns(self) -> Dict:
|
||
"""Паттерны по умолчанию"""
|
||
return {
|
||
'patterns': {
|
||
'new_account_days': 7,
|
||
'no_avatar_score': 10,
|
||
'generic_username_score': 15,
|
||
'unverified_hours': 24,
|
||
'unverified_score': 25,
|
||
'provocation_score': 30,
|
||
'office_hours_score': 15,
|
||
'multicount_score': 50,
|
||
},
|
||
'thresholds': {
|
||
'normal': 30,
|
||
'suspicious': 60,
|
||
},
|
||
'office_hours': {
|
||
'start': 9,
|
||
'end': 18,
|
||
'weekdays': [0, 1, 2, 3, 4],
|
||
}
|
||
}
|
||
|
||
async def analyze_user(self, user: User) -> Tuple[int, List[str]]:
|
||
"""
|
||
Анализ пользователя на подозрительность
|
||
Возвращает: (score, list_of_flags)
|
||
"""
|
||
score = 0
|
||
flags = []
|
||
patterns = self.patterns['patterns']
|
||
|
||
# 1. Новый аккаунт
|
||
# Примечание: Telegram не отдаёт дату создания аккаунта через Bot API
|
||
# Можно эвристически определять по первым сообщениям
|
||
|
||
# 2. Нет аватарки (проверяем по наличию username/фото)
|
||
if not user.username or user.username.startswith('user'):
|
||
score += patterns.get('no_avatar_score', 10)
|
||
flags.append('no_avatar')
|
||
|
||
# 3. Стандартный никнейм
|
||
if self._is_generic_username(user.username):
|
||
score += patterns.get('generic_username_score', 15)
|
||
flags.append('generic_username')
|
||
|
||
# 4. Не прошёл верификацию
|
||
if not user.verified:
|
||
hours_since_join = (datetime.utcnow() - user.join_date).total_seconds() / 3600
|
||
if hours_since_join > patterns.get('unverified_hours', 24):
|
||
score += patterns.get('unverified_score', 25)
|
||
flags.append('unverified')
|
||
|
||
# 5. Активность только в рабочие часы
|
||
is_office = await self._check_office_hours_activity(user.user_id)
|
||
if is_office:
|
||
score += patterns.get('office_hours_score', 15)
|
||
flags.append('office_hours')
|
||
|
||
# 6. Провокационные сообщения
|
||
has_provocation = await self._check_provocations(user.user_id)
|
||
if has_provocation:
|
||
score += patterns.get('provocation_score', 30)
|
||
flags.append('provocation')
|
||
|
||
# Ограничиваем score до 100
|
||
score = min(score, 100)
|
||
|
||
return score, flags
|
||
|
||
async def _check_office_hours_activity(self, user_id: int) -> bool:
|
||
"""
|
||
Проверка: активность только в рабочие часы (9-18, пн-пт)
|
||
"""
|
||
stmt = (
|
||
select(func.count())
|
||
.select_from(Message)
|
||
.where(Message.user_id == user_id)
|
||
)
|
||
result = await self.session.execute(stmt)
|
||
total = result.scalar() or 0
|
||
|
||
if total == 0:
|
||
return False
|
||
|
||
# Считаем сообщения в рабочие часы
|
||
# Примечание: для точной проверки нужно хранить время каждого сообщения
|
||
# и анализировать распределение
|
||
|
||
# Упрощённая версия: если >80% сообщений в рабочие часы
|
||
# Требуется дополнительная логика анализа временных меток
|
||
|
||
return False # Заглушка
|
||
|
||
async def _check_provocations(self, user_id: int) -> bool:
|
||
"""
|
||
Проверка на провокационные сообщения в пользу УК
|
||
"""
|
||
keywords = self.patterns.get('keywords_provocation', [
|
||
'управляющая компания права',
|
||
'ук говорит что',
|
||
'надо платить больше',
|
||
])
|
||
|
||
# Ищем сообщения с ключевыми словами
|
||
for keyword in keywords:
|
||
stmt = (
|
||
select(func.count())
|
||
.select_from(Message)
|
||
.where(Message.user_id == user_id)
|
||
.where(Message.text.ilike(f'%{keyword}%'))
|
||
)
|
||
result = await self.session.execute(stmt)
|
||
count = result.scalar() or 0
|
||
if count > 0:
|
||
return True
|
||
|
||
return False
|
||
|
||
def _is_generic_username(self, username: str) -> bool:
|
||
"""Проверка на стандартный никнейм"""
|
||
if not username:
|
||
return True
|
||
|
||
import re
|
||
patterns = [
|
||
r'^user\d+$',
|
||
r'^user_\w+$',
|
||
r'^id\d+$',
|
||
]
|
||
return any(re.match(p, username.lower()) for p in patterns)
|
||
|
||
async def update_spy_score(self, user: User) -> bool:
|
||
"""
|
||
Обновить рейтинг подозрительности пользователя
|
||
Возвращает True, если score изменился значительно
|
||
"""
|
||
old_score = user.spy_score
|
||
new_score, flags = await self.analyze_user(user)
|
||
|
||
user.spy_score = new_score
|
||
user.spy_flags = json.dumps(flags)
|
||
|
||
# Логгируем изменение
|
||
if abs(new_score - old_score) >= 20:
|
||
await self._log_spy_detection(user, new_score, flags)
|
||
return True
|
||
|
||
return False
|
||
|
||
async def _log_spy_detection(self, user: User, score: int, flags: List[str]):
|
||
"""Логирование детекта"""
|
||
spy_log = SpyLog(
|
||
user_id=user.user_id,
|
||
spy_score=score,
|
||
flags=json.dumps(flags),
|
||
)
|
||
self.session.add(spy_log)
|
||
await self.session.commit()
|
||
|
||
async def get_suspicious_users(self, min_score: int = 30) -> List[User]:
|
||
"""Получить список подозрительных пользователей"""
|
||
stmt = (
|
||
select(User)
|
||
.where(User.spy_score >= min_score)
|
||
.order_by(User.spy_score.desc())
|
||
)
|
||
result = await self.session.execute(stmt)
|
||
return list(result.scalars().all())
|
||
|
||
async def get_spies(self) -> List[User]:
|
||
"""Получить пользователей со статусом «шпион» (score > 60)"""
|
||
return await self.get_suspicious_users(min_score=61)
|