✅ Регистрация и верификация жильцов ✅ Рейтинг активности (уровни) ✅ Детект шпионов (Score 0-100) ✅ Анти-мат система (3 предупреждения → бан) ✅ Админ-панель с рассылкой ✅ Учёт квартир (несколько жильцов) ✅ Телефоны экстренных служб и мастеров ✅ Еженедельный экспорт JSON ✅ Прокси (socks5) для обхода РКН Бекап: backups/versions/v1.0_mvp_2026-02-26/ Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
244 lines
9.2 KiB
Python
244 lines
9.2 KiB
Python
"""
|
||
Детект мультиаккаунтов — выявление нескольких аккаунтов от одного человека
|
||
Только для администраторов
|
||
"""
|
||
import logging
|
||
from aiogram import Router, F
|
||
from aiogram.types import Message
|
||
from aiogram.filters import Command
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select
|
||
from database.db import AsyncSessionLocal
|
||
from database.models import User
|
||
from services.multicount_detector import MulticountDetector
|
||
from utils.decorators import admin_only
|
||
import config
|
||
import json
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = Router()
|
||
|
||
|
||
# =============================================================================
|
||
# ПРОВЕРКА НА МУЛЬТИАККАУНТЫ
|
||
# =============================================================================
|
||
|
||
@router.message(Command('multicheck'))
|
||
@router.message(Command('мультичек'))
|
||
@admin_only
|
||
async def cmd_multicheck(message: Message):
|
||
"""
|
||
Проверить всех пользователей на мультиаккаунты
|
||
Использование: /multicheck
|
||
"""
|
||
await message.answer('🔍 Анализирую пользователей на мультиаккаунты...')
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
detector = MulticountDetector(session)
|
||
suspicious = await detector.analyze_all_users()
|
||
|
||
if not suspicious:
|
||
await message.answer('✅ Мультиаккаунты не обнаружены')
|
||
return
|
||
|
||
text = f'🚨 <b>Обнаружены подозрительные пары</b> ({len(suspicious)})\n\n'
|
||
|
||
for i, pair in enumerate(suspicious, 1):
|
||
text += f'{i}. <b>Пара #{i}</b>\n'
|
||
text += f' ├─ {pair["user1_name"]} (кв. {pair["user1_apartment"] or "?"})\n'
|
||
text += f' └─ {pair["user2_name"]} (кв. {pair["user2_apartment"] or "?"})\n'
|
||
text += f' └─ Score: {pair["score"]}/100\n'
|
||
text += f' └─ Причины: {", ".join(pair["reasons"])}\n\n'
|
||
|
||
text += '💡 Для проверки квартиры: /check_apt <номер>'
|
||
|
||
await message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.message(Command('check_apt'))
|
||
@router.message(Command('проверить_квартиру'))
|
||
@admin_only
|
||
async def cmd_check_apartment(message: Message):
|
||
"""
|
||
Проверить квартиру на мультиаккаунты
|
||
Использование: /check_apt <номер>
|
||
"""
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/check_apt <номер квартиры></code>\n\n'
|
||
'Пример: /check_apt 45',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
apartment = args[1]
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
# Получаем всех пользователей в квартире
|
||
stmt = select(User).where(User.apartment == apartment)
|
||
result = await session.execute(stmt)
|
||
users = list(result.scalars().all())
|
||
|
||
if not users:
|
||
await message.answer(f'ℹ️ В квартире {apartment} нет зарегистрированных пользователей')
|
||
return
|
||
|
||
if len(users) == 1:
|
||
user = users[0]
|
||
text = (
|
||
f'🏠 <b>Квартира {apartment}</b>\n\n'
|
||
f'1 жилец:\n'
|
||
f'• {user.full_name} (@{user.username or "нет ника"})\n'
|
||
f' └─ ID: {user.user_id}\n'
|
||
f' └─ Верифицирован: {"✅" if user.verified else "❌"}\n'
|
||
f' └─ Сообщений: {user.message_count}\n'
|
||
)
|
||
await message.answer(text, parse_mode='HTML')
|
||
return
|
||
|
||
# Несколько жильцов
|
||
text = f'🏠 <b>Квартира {apartment}</b> — {len(users)} жильцов\n\n'
|
||
|
||
for i, user in enumerate(users, 1):
|
||
text += f'{i}. {user.full_name} (@{user.username or "нет ника"})\n'
|
||
text += f' └─ ID: {user.user_id}\n'
|
||
text += f' └─ Верифицирован: {"✅" if user.verified else "❌"}\n'
|
||
text += f' └─ Сообщений: {user.message_count}\n'
|
||
text += f' └─ Рейтинг: {user.rating}\n\n'
|
||
|
||
if len(users) > 2:
|
||
text += f'⚠️ <b>Внимание!</b> В квартире {len(users)} человек — это много!\n\n'
|
||
|
||
text += '💡 Для детальной проверки: /multicheck'
|
||
|
||
await message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.message(Command('multistats'))
|
||
@admin_only
|
||
async def cmd_multistats(message: Message):
|
||
"""Показать статистику по мультиаккаунтам"""
|
||
async with AsyncSessionLocal() as session:
|
||
detector = MulticountDetector(session)
|
||
|
||
# Получаем пользователей с multicount_score > 0
|
||
stmt = select(User).where(User.multicount_score > 0)
|
||
result = await session.execute(stmt)
|
||
flagged_users = list(result.scalars().all())
|
||
|
||
# Считаем квартиры с несколькими жильцами
|
||
stmt = select(User.apartment, func.count(User.user_id)).where(
|
||
User.apartment.isnot(None)
|
||
).group_by(User.apartment).having(func.count(User.user_id) > 1)
|
||
result = await session.execute(stmt)
|
||
multi_apt = list(result.all())
|
||
|
||
text = '📊 <b>Статистика мультиаккаунтов</b>\n\n'
|
||
|
||
if flagged_users:
|
||
text += f'🔴 Пользователей с подозрительным score: {len(flagged_users)}\n\n'
|
||
for user in flagged_users[:10]:
|
||
text += f'• {user.full_name} (кв. {user.apartment or "?"}) — score: {user.multicount_score}\n'
|
||
if len(flagged_users) > 10:
|
||
text += f'\n... и ещё {len(flagged_users) - 10}'
|
||
text += '\n\n'
|
||
|
||
if multi_apt:
|
||
text += f'🏠 Квартир с несколькими жильцами: {len(multi_apt)}\n\n'
|
||
for apt, count in sorted(multi_apt, key=lambda x: x[1], reverse=True)[:10]:
|
||
text += f'• Кв. {apt} — {count} чел.\n'
|
||
if len(multi_apt) > 10:
|
||
text += f'\n... и ещё {len(multi_apt) - 10}'
|
||
|
||
if not flagged_users and not multi_apt:
|
||
text += '✅ Подозрительных активностей не обнаружено'
|
||
|
||
await message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.message(Command('clear_multiscore'))
|
||
@admin_only
|
||
async def cmd_clear_multiscore(message: Message):
|
||
"""
|
||
Сбросить multicount_score пользователю
|
||
Использование: /clear_multiscore <user_id>
|
||
"""
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/clear_multiscore <user_id></code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
try:
|
||
user_id = int(args[1])
|
||
except ValueError:
|
||
await message.answer('❌ Неверный ID пользователя')
|
||
return
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
user = await session.get(User, user_id)
|
||
|
||
if not user:
|
||
await message.answer(f'❌ Пользователь {user_id} не найден')
|
||
return
|
||
|
||
user.multicount_score = 0
|
||
user.suspected_alt_accounts = None
|
||
await session.commit()
|
||
|
||
await message.answer(f'✅ multicount_score пользователя {user_id} сброшен')
|
||
|
||
|
||
@router.message(Command('link_accounts'))
|
||
@admin_only
|
||
async def cmd_link_accounts(message: Message):
|
||
"""
|
||
Связать аккаунты (официально, например семья)
|
||
Использование: /link_accounts <user_id1> <user_id2>
|
||
"""
|
||
args = message.text.split()
|
||
if len(args) < 3:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/link_accounts <user_id1> <user_id2></code>\n\n'
|
||
'Пример: /link_accounts 123456789 987654321',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
try:
|
||
user_id1 = int(args[1])
|
||
user_id2 = int(args[2])
|
||
except ValueError:
|
||
await message.answer('❌ Неверные ID пользователей')
|
||
return
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
user1 = await session.get(User, user_id1)
|
||
user2 = await session.get(User, user_id2)
|
||
|
||
if not user1 or not user2:
|
||
await message.answer('❌ Один из пользователей не найден')
|
||
return
|
||
|
||
# Сбрасываем score
|
||
user1.multicount_score = 0
|
||
user2.multicount_score = 0
|
||
|
||
# Помечаем что аккаунты связаны (это не шпионы)
|
||
# Можно добавить отдельную таблицу linked_accounts
|
||
|
||
await session.commit()
|
||
|
||
await message.answer(
|
||
f'✅ Аккаунты связаны:\n'
|
||
f'• {user1.full_name} (ID: {user_id1})\n'
|
||
f'• {user2.full_name} (ID: {user_id2})\n\n'
|
||
f'multicount_score сброшен'
|
||
)
|