""" Хендлеры безопасности: бан, отзыв верификации, логирование """ 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 [причина] """ args = message.text.split(maxsplit=2) if len(args) < 2: await message.answer( 'Использование: /unverify <user_id> [причина]\n\n' 'Пример: /unverify 123456789 Подозрение на шпиона', 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'⚠️ Верификация отозвана\n\n' f'Причина: {reason}\n\n' f'❗ Что дальше:\n' f'У вас есть 7 дней на повторную верификацию.\n' f'Если не пройдёте верификацию — будете удалены из чата.\n\n' f'Для верификации напишите боту номер квартиры в личку.', parse_mode='HTML' ) except Exception as e: logger.error(f'Не удалось уведомить пользователя: {e}') # Отвечаем админу await message.answer( f'✅ Верификация отозвана\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 [причина] """ args = message.text.split(maxsplit=2) if len(args) < 2: await message.answer('Использование: /ban <user_id> [причина]', 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'🚫 Пользователь забанен и удалён из чата\n\n' f'Пользователь: {user.full_name}\n' f'Причина: {reason}', parse_mode='HTML' ) except Exception as e: logger.error(f'Не удалось удалить из чата: {e}') await message.answer( f'⚠️ Пользователь забанен (в БД)\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('Использование: /unban <user_id>', 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'✅ Пользователь {user.full_name} разбанен', parse_mode='HTML') # ============================================================================= # ЛОГИРОВАНИЕ ПОДОЗРИТЕЛЬНЫХ # ============================================================================= @router.message(Command('log_spy')) @admin_only async def cmd_log_spy(message: Message): """ Создать лог подозрительной активности Использование: /log_spy [комментарий] """ args = message.text.split(maxsplit=2) if len(args) < 2: await message.answer('Использование: /log_spy <user_id> [комментарий]', 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'📝 Лог создан\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'🚫 Забаненные пользователи ({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'⏳ С отозванной верификацией ({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'⚠️ Не верифицированы более 7 дней ({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💡 Используйте /unverify <user_id> для отзыва' await message.answer(text, parse_mode='HTML')