✅ Регистрация и верификация жильцов ✅ Рейтинг активности (уровни) ✅ Детект шпионов (Score 0-100) ✅ Анти-мат система (3 предупреждения → бан) ✅ Админ-панель с рассылкой ✅ Учёт квартир (несколько жильцов) ✅ Телефоны экстренных служб и мастеров ✅ Еженедельный экспорт JSON ✅ Прокси (socks5) для обхода РКН Бекап: backups/versions/v1.0_mvp_2026-02-26/ Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
419 lines
16 KiB
Python
419 lines
16 KiB
Python
"""
|
||
Хендлеры безопасности: бан, отзыв верификации, логирование
|
||
"""
|
||
import logging
|
||
import json
|
||
from datetime import datetime, timedelta
|
||
from aiogram import Router, F
|
||
from aiogram.types import Message, CallbackQuery, FSInputFile
|
||
from aiogram.filters import Command
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select, update
|
||
from database.db import AsyncSessionLocal
|
||
from database.models import User, VerificationRequest, SpyLog
|
||
from utils.decorators import admin_only
|
||
from utils.formatters import format_user_info
|
||
import config
|
||
import os
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = Router()
|
||
|
||
|
||
# =============================================================================
|
||
# ОТЗЫВ ВЕРИФИКАЦИИ (UNVERIFY)
|
||
# =============================================================================
|
||
|
||
@router.message(Command('unverify'))
|
||
@admin_only
|
||
async def cmd_unverify(message: Message):
|
||
"""
|
||
Отзыв верификации у пользователя
|
||
Использование: /unverify <user_id> [причина]
|
||
"""
|
||
args = message.text.split(maxsplit=2)
|
||
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'Использование: <code>/unverify <user_id> [причина]</code>\n\n'
|
||
'Пример: <code>/unverify 123456789 Подозрение на шпиона</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
try:
|
||
user_id = int(args[1])
|
||
except ValueError:
|
||
await message.answer('❌ Неверный ID пользователя')
|
||
return
|
||
|
||
reason = args[2] if len(args) > 2 else 'Без указания причины'
|
||
|
||
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:
|
||
await message.answer('❌ Пользователь не найден')
|
||
return
|
||
|
||
# Проверяем, не админ ли это
|
||
if user.is_admin:
|
||
await message.answer('❌ Нельзя отозвать верификацию у администратора!')
|
||
return
|
||
|
||
# Если уже не верифицирован
|
||
if not user.verified:
|
||
await message.answer(f'ℹ️ Пользователь {user.full_name} уже не верифицирован')
|
||
return
|
||
|
||
# Отзываем верификацию
|
||
user.verified = False
|
||
|
||
# Создаём запись в логе
|
||
verification_log = VerificationRequest(
|
||
user_id=user_id,
|
||
apartment=user.apartment,
|
||
status='revoked',
|
||
comment=f'Отзыв верификации: {reason}',
|
||
reviewed_at=datetime.utcnow(),
|
||
reviewed_by=message.from_user.id,
|
||
)
|
||
session.add(verification_log)
|
||
|
||
await session.commit()
|
||
|
||
logger.info(f'Верификация отозвана у user_id={user_id}, причина: {reason}')
|
||
|
||
# Уведомляем пользователя (в личку)
|
||
try:
|
||
await message.bot.send_message(
|
||
user_id,
|
||
f'⚠️ <b>Верификация отозвана</b>\n\n'
|
||
f'Причина: {reason}\n\n'
|
||
f'❗ <b>Что дальше:</b>\n'
|
||
f'У вас есть <b>7 дней</b> на повторную верификацию.\n'
|
||
f'Если не пройдёте верификацию — будете удалены из чата.\n\n'
|
||
f'Для верификации напишите боту номер квартиры в личку.',
|
||
parse_mode='HTML'
|
||
)
|
||
except Exception as e:
|
||
logger.error(f'Не удалось уведомить пользователя: {e}')
|
||
|
||
# Отвечаем админу
|
||
await message.answer(
|
||
f'✅ <b>Верификация отозвана</b>\n\n'
|
||
f'Пользователь: {user.full_name}\n'
|
||
f'Квартира: {user.apartment}\n'
|
||
f'Причина: {reason}\n\n'
|
||
f'⏳ У пользователя есть 7 дней на повторную верификацию',
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# БАН / РАЗБАН
|
||
# =============================================================================
|
||
|
||
@router.message(Command('ban'))
|
||
@admin_only
|
||
async def cmd_ban(message: Message):
|
||
"""
|
||
Бан пользователя (помечает в БД)
|
||
Использование: /ban <user_id> [причина]
|
||
"""
|
||
args = message.text.split(maxsplit=2)
|
||
|
||
if len(args) < 2:
|
||
await message.answer('Использование: <code>/ban <user_id> [причина]</code>', parse_mode='HTML')
|
||
return
|
||
|
||
try:
|
||
user_id = int(args[1])
|
||
except ValueError:
|
||
await message.answer('❌ Неверный ID пользователя')
|
||
return
|
||
|
||
reason = args[2] if len(args) > 2 else 'Нарушение правил чата'
|
||
|
||
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:
|
||
await message.answer('❌ Пользователь не найден')
|
||
return
|
||
|
||
# Помечаем как забаненный
|
||
user.is_banned = True
|
||
|
||
# Создаём лог
|
||
spy_log = SpyLog(
|
||
user_id=user_id,
|
||
spy_score=100,
|
||
flags=json.dumps(['banned_by_admin', reason]),
|
||
action_taken='banned',
|
||
)
|
||
session.add(spy_log)
|
||
|
||
await session.commit()
|
||
|
||
logger.info(f'Пользователь забанен: user_id={user_id}, причина: {reason}')
|
||
|
||
# Пытаемся удалить из чата
|
||
try:
|
||
await message.chat.ban(user_id)
|
||
await message.answer(
|
||
f'🚫 <b>Пользователь забанен и удалён из чата</b>\n\n'
|
||
f'Пользователь: {user.full_name}\n'
|
||
f'Причина: {reason}',
|
||
parse_mode='HTML'
|
||
)
|
||
except Exception as e:
|
||
logger.error(f'Не удалось удалить из чата: {e}')
|
||
await message.answer(
|
||
f'⚠️ <b>Пользователь забанен (в БД)</b>\n\n'
|
||
f'Пользователь: {user.full_name}\n'
|
||
f'Причина: {reason}\n\n'
|
||
f'❌ Не удалось удалить из чата автоматически.\n'
|
||
f'Удалите вручную или дайте боту права администратора.',
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.message(Command('unban'))
|
||
@admin_only
|
||
async def cmd_unban(message: Message):
|
||
"""Разбан пользователя"""
|
||
args = message.text.split(maxsplit=2)
|
||
|
||
if len(args) < 2:
|
||
await message.answer('Использование: <code>/unban <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:
|
||
stmt = select(User).where(User.user_id == user_id)
|
||
result = await session.execute(stmt)
|
||
user = result.scalar_one_or_none()
|
||
|
||
if not user:
|
||
await message.answer('❌ Пользователь не найден')
|
||
return
|
||
|
||
user.is_banned = False
|
||
await session.commit()
|
||
|
||
logger.info(f'Пользователь разбанен: user_id={user_id}')
|
||
|
||
await message.answer(f'✅ <b>Пользователь {user.full_name} разбанен</b>', parse_mode='HTML')
|
||
|
||
|
||
# =============================================================================
|
||
# ЛОГИРОВАНИЕ ПОДОЗРИТЕЛЬНЫХ
|
||
# =============================================================================
|
||
|
||
@router.message(Command('log_spy'))
|
||
@admin_only
|
||
async def cmd_log_spy(message: Message):
|
||
"""
|
||
Создать лог подозрительной активности
|
||
Использование: /log_spy <user_id> [комментарий]
|
||
"""
|
||
args = message.text.split(maxsplit=2)
|
||
|
||
if len(args) < 2:
|
||
await message.answer('Использование: <code>/log_spy <user_id> [комментарий]</code>', parse_mode='HTML')
|
||
return
|
||
|
||
try:
|
||
user_id = int(args[1])
|
||
except ValueError:
|
||
await message.answer('❌ Неверный ID пользователя')
|
||
return
|
||
|
||
comment = args[2] if len(args) > 2 else 'Подозрительная активность'
|
||
|
||
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:
|
||
await message.answer('❌ Пользователь не найден')
|
||
return
|
||
|
||
# Создаём лог
|
||
spy_log = SpyLog(
|
||
user_id=user_id,
|
||
spy_score=user.spy_score + 20, # Увеличиваем score
|
||
flags=json.dumps(['manual_log_by_admin', comment]),
|
||
action_taken='logged',
|
||
)
|
||
session.add(spy_log)
|
||
|
||
# Обновляем score пользователя
|
||
user.spy_score = min(user.spy_score + 20, 100)
|
||
|
||
await session.commit()
|
||
|
||
# Сохраняем лог в файл
|
||
await save_spy_log_to_file(user, comment)
|
||
|
||
logger.info(f'Создан лог подозрительности: user_id={user_id}')
|
||
|
||
await message.answer(
|
||
f'📝 <b>Лог создан</b>\n\n'
|
||
f'Пользователь: {user.full_name}\n'
|
||
f'Квартира: {user.apartment}\n'
|
||
f'Комментарий: {comment}\n'
|
||
f'Spy Score: {user.spy_score}/100',
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
async def save_spy_log_to_file(user: User, comment: str):
|
||
"""Сохранение лога в файл"""
|
||
try:
|
||
log_dir = 'logs/spy_logs'
|
||
os.makedirs(log_dir, exist_ok=True)
|
||
|
||
# Имя файла: user_id_username_date.log
|
||
username = user.username or f'user{user.user_id}'
|
||
timestamp = datetime.utcnow().strftime('%Y%m%d_%H%M%S')
|
||
filename = f'{log_dir}/{user.user_id}_{username}_{timestamp}.log'
|
||
|
||
log_text = f'''=== ЛОГ ПОДОЗРИТЕЛЬНОЙ АКТИВНОСТИ ===
|
||
Дата: {datetime.utcnow().strftime('%d.%m.%Y %H:%M')}
|
||
Пользователь: {user.full_name}
|
||
ID: {user.user_id}
|
||
Username: @{user.username or 'нет'}
|
||
Квартира: {user.apartment or 'не указана'}
|
||
Spy Score: {user.spy_score}/100
|
||
|
||
Комментарий админа:
|
||
{comment}
|
||
|
||
=== История сообщений (последние 50) ===
|
||
(см. в основном экспорте)
|
||
|
||
=== Статус ===
|
||
Верифицирован: {"Да" if user.verified else "Нет"}
|
||
Забанен: {"Да" if user.is_banned else "Нет"}
|
||
'''
|
||
|
||
with open(filename, 'w', encoding='utf-8') as f:
|
||
f.write(log_text)
|
||
|
||
logger.info(f'Лог сохранён: {filename}')
|
||
|
||
except Exception as e:
|
||
logger.error(f'Не удалось сохранить лог: {e}')
|
||
|
||
|
||
# =============================================================================
|
||
# СПИСОК ЗАБАНЕННЫХ / ОТОЗВАННЫХ
|
||
# =============================================================================
|
||
|
||
@router.message(Command('banned'))
|
||
@admin_only
|
||
async def cmd_banned_list(message: Message):
|
||
"""Список забаненных пользователей"""
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(User).where(User.is_banned == True)
|
||
result = await session.execute(stmt)
|
||
users = list(result.scalars().all())
|
||
|
||
if not users:
|
||
await message.answer('✅ Забаненных пользователей нет')
|
||
return
|
||
|
||
text = f'🚫 <b>Забаненные пользователи</b> ({len(users)}):\n\n'
|
||
for user in users:
|
||
text += f'• {user.full_name} (кв. {user.apartment}) — ID: {user.user_id}\n'
|
||
|
||
await message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.message(Command('unverified_list'))
|
||
@admin_only
|
||
async def cmd_unverified_list(message: Message):
|
||
"""Список пользователей с отозванной верификацией"""
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = (
|
||
select(User)
|
||
.where(User.verified == False)
|
||
.where(User.is_banned == False)
|
||
.order_by(User.verification_date.desc())
|
||
)
|
||
result = await session.execute(stmt)
|
||
users = list(result.scalars().all())
|
||
|
||
# Фильтруем тех у кого вообще не было верификации
|
||
users_with_revoked = [u for u in users if u.verification_date is not None]
|
||
|
||
if not users_with_revoked:
|
||
await message.answer('✅ Нет пользователей с отозванной верификацией')
|
||
return
|
||
|
||
text = f'⏳ <b>С отозванной верификацией</b> ({len(users_with_revoked)}):\n\n'
|
||
for user in users_with_revoked[:20]: # Показываем первые 20
|
||
days_since = (datetime.utcnow() - user.verification_date).days if user.verification_date else '?'
|
||
text += f'• {user.full_name} (кв. {user.apartment}) — отозвано {days_since} дн. назад\n'
|
||
|
||
if len(users_with_revoked) > 20:
|
||
text += f'\n... и ещё {len(users_with_revoked) - 20}'
|
||
|
||
await message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
# =============================================================================
|
||
# АВТО-УДАЛЕНИЕ НЕВЕРИФИЦИРОВАННЫХ
|
||
# =============================================================================
|
||
|
||
@router.message(Command('check_unverified'))
|
||
@admin_only
|
||
async def cmd_check_unverified(message: Message):
|
||
"""
|
||
Проверка пользователей с просроченной верификацией
|
||
Показывает кто не прошёл верификацию за 7 дней
|
||
"""
|
||
async with AsyncSessionLocal() as session:
|
||
# Находим пользователей которые не верифицированы более 7 дней
|
||
week_ago = datetime.utcnow() - timedelta(days=7)
|
||
|
||
stmt = (
|
||
select(User)
|
||
.where(User.verified == False)
|
||
.where(User.join_date <= week_ago)
|
||
.where(User.is_banned == False)
|
||
)
|
||
result = await session.execute(stmt)
|
||
users = list(result.scalars().all())
|
||
|
||
if not users:
|
||
await message.answer('✅ Все пользователи верифицированы вовремя')
|
||
return
|
||
|
||
text = f'⚠️ <b>Не верифицированы более 7 дней</b> ({len(users)}):\n\n'
|
||
for user in users[:20]:
|
||
days = (datetime.utcnow() - user.join_date).days
|
||
text += f'• {user.full_name} (кв. {user.apartment}) — {days} дн. в чате\n'
|
||
|
||
if len(users) > 20:
|
||
text += f'\n... и ещё {len(users) - 20}'
|
||
|
||
text += '\n\n💡 Используйте <code>/unverify <user_id></code> для отзыва'
|
||
|
||
await message.answer(text, parse_mode='HTML')
|