domovoy_bot/handlers/security.py
Admin ba4676b0af v1.0 MVP - Инициализация проекта 🏠
 Регистрация и верификация жильцов
 Рейтинг активности (уровни)
 Детект шпионов (Score 0-100)
 Анти-мат система (3 предупреждения → бан)
 Админ-панель с рассылкой
 Учёт квартир (несколько жильцов)
 Телефоны экстренных служб и мастеров
 Еженедельный экспорт JSON
 Прокси (socks5) для обхода РКН

Бекап: backups/versions/v1.0_mvp_2026-02-26/

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-27 10:54:16 +00:00

419 lines
16 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Хендлеры безопасности: бан, отзыв верификации, логирование
"""
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 &lt;user_id&gt; [причина]</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 &lt;user_id&gt; [причина]</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 &lt;user_id&gt;</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 &lt;user_id&gt; [комментарий]</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 &lt;user_id&gt;</code> для отзыва'
await message.answer(text, parse_mode='HTML')