✅ Регистрация и верификация жильцов ✅ Рейтинг активности (уровни) ✅ Детект шпионов (Score 0-100) ✅ Анти-мат система (3 предупреждения → бан) ✅ Админ-панель с рассылкой ✅ Учёт квартир (несколько жильцов) ✅ Телефоны экстренных служб и мастеров ✅ Еженедельный экспорт JSON ✅ Прокси (socks5) для обхода РКН Бекап: backups/versions/v1.0_mvp_2026-02-26/ Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
425 lines
18 KiB
Python
425 lines
18 KiB
Python
"""
|
||
Хендлер верификации жильцов
|
||
"""
|
||
import logging
|
||
from aiogram import Router, F
|
||
from aiogram.types import Message, CallbackQuery
|
||
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, VerificationRequest
|
||
from utils.validators import validate_apartment
|
||
from utils.decorators import admin_only, admin_callback_only
|
||
import config
|
||
from datetime import datetime
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = Router()
|
||
|
||
|
||
@router.message(Command('верификация'))
|
||
@router.message(Command('verification'))
|
||
async def cmd_verification(message: Message):
|
||
"""Команда /верификация - статус верификации"""
|
||
logger.info(f'Команда /верификация от user_id={message.from_user.id}')
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(User).where(User.user_id == message.from_user.id)
|
||
result = await session.execute(stmt)
|
||
user = result.scalar_one_or_none()
|
||
|
||
if not user:
|
||
await message.answer(
|
||
'❌ Вы не зарегистрированы.\n\n'
|
||
'Нажмите /start или отправьте номер квартиры.'
|
||
)
|
||
return
|
||
|
||
if user.verified:
|
||
await message.answer(
|
||
f'✅ Вы верифицированы\n\n'
|
||
f'Квартира: <code>{user.apartment}</code>\n'
|
||
f'Сообщений: {user.message_count}',
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
# Проверяем, есть ли заявка
|
||
stmt = select(VerificationRequest).where(
|
||
VerificationRequest.user_id == message.from_user.id,
|
||
VerificationRequest.status == 'pending'
|
||
)
|
||
result = await session.execute(stmt)
|
||
request = result.scalar_one_or_none()
|
||
|
||
if request:
|
||
await message.answer(
|
||
f'⏳ <b>Заявка на проверке</b>\n\n'
|
||
f'Квартира: <code>{request.apartment}</code>\n'
|
||
f'Статус: Ожидает подтверждения\n\n'
|
||
f'Администратор скоро подтвердит вашу заявку.',
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
await message.answer(
|
||
f'⏳ Вы не верифицированы\n\n'
|
||
f'📝 <b>Как пройти верификацию:</b>\n\n'
|
||
f'1. Отправьте номер квартиры текстом\n'
|
||
f'2. Дождитесь подтверждения администратора\n\n'
|
||
f'Примеры: <code>45</code>, <code>12А</code>, <code>100-1</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.message(F.text.regexp(r'^\s*\d+[А-ЯA-Z]?[\s/-]?\d*[А-ЯA-Z]?\s*$'))
|
||
async def handle_apartment_number(message: Message):
|
||
"""
|
||
Обработка номера квартиры
|
||
Срабатывает и в личке, и в группе
|
||
"""
|
||
text = message.text.strip()
|
||
user_id = message.from_user.id
|
||
|
||
# Если админ отправил номер квартиры — автоматически верифицируем
|
||
if config.is_admin(user_id):
|
||
logger.info(f'Админ отправил номер квартиры: {text}, автоматически верифицируем')
|
||
await auto_verify_admin(message, text)
|
||
return
|
||
|
||
logger.info(f'Получен номер квартиры: {text} от user_id={user_id}')
|
||
|
||
# Проверяем валидность
|
||
if not validate_apartment(text):
|
||
logger.info(f'Неверный формат квартиры: {text}')
|
||
return # Просто игнорируем, это не номер квартиры
|
||
|
||
await process_verification(message, text)
|
||
|
||
|
||
async def auto_verify_admin(message: Message, apartment: str):
|
||
"""Автоматическая верификация админа"""
|
||
user_id = message.from_user.id
|
||
apartment_upper = apartment.strip().upper()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
try:
|
||
# Проверяем пользователя в БД
|
||
stmt = select(User).where(User.user_id == user_id)
|
||
result = await session.execute(stmt)
|
||
user = result.scalar_one_or_none()
|
||
|
||
if not user:
|
||
# Создаём нового пользователя (админа)
|
||
user = User(
|
||
user_id=user_id,
|
||
username=message.from_user.username,
|
||
first_name=message.from_user.first_name,
|
||
last_name=message.from_user.last_name,
|
||
apartment=apartment_upper,
|
||
verified=True,
|
||
is_admin=True,
|
||
verification_date=datetime.utcnow(),
|
||
)
|
||
session.add(user)
|
||
logger.info(f'Создан новый админ с квартирой {apartment_upper}')
|
||
else:
|
||
# Обновляем данные админа
|
||
user.apartment = apartment_upper
|
||
user.verified = True
|
||
user.is_admin = True
|
||
if not user.verification_date:
|
||
user.verification_date = datetime.utcnow()
|
||
logger.info(f'Админ верифицирован с квартирой {apartment_upper}')
|
||
|
||
await session.commit()
|
||
|
||
await message.answer(
|
||
f'✅ <b>Администратор верифицирован!</b>\n\n'
|
||
f'Квартира: <code>{apartment_upper}</code>\n'
|
||
f'Статус: <b>Владелец/Админ</b>\n\n'
|
||
f'Теперь вы можете использовать все админские функции.',
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f'Ошибка при авто-верификации админа: {e}', exc_info=True)
|
||
await message.answer('❌ Произошла ошибка при верификации админа.')
|
||
|
||
|
||
@router.message(F.text, F.chat.type == 'private')
|
||
async def handle_any_text_in_private(message: Message):
|
||
"""
|
||
Обработка любого текста в личке
|
||
Пытаемся распознать номер квартиры
|
||
"""
|
||
# Пропускаем если это админ
|
||
if config.is_admin(message.from_user.id):
|
||
return
|
||
|
||
text = message.text.strip()
|
||
logger.info(f'Текст в личке: {text} от user_id={message.from_user.id}')
|
||
|
||
# Проверяем, похоже ли на номер квартиры
|
||
if validate_apartment(text):
|
||
logger.info(f'Распознан номер квартиры: {text}')
|
||
await process_verification(message, text)
|
||
else:
|
||
# Если не похоже на квартиру — подсказываем
|
||
logger.info(f'Не похоже на номер квартиры: {text}')
|
||
# Не отвечаем на каждое сообщение, чтобы не спамить
|
||
|
||
|
||
async def process_verification(message: Message, apartment: str):
|
||
"""Обработка верификации"""
|
||
user_id = message.from_user.id
|
||
apartment_upper = apartment.strip().upper()
|
||
|
||
logger.info(f'Начало верификации: user_id={user_id}, apartment={apartment_upper}')
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
try:
|
||
# Проверяем пользователя в БД
|
||
stmt = select(User).where(User.user_id == user_id)
|
||
result = await session.execute(stmt)
|
||
user = result.scalar_one_or_none()
|
||
|
||
if not user:
|
||
logger.info(f'Создаём нового пользователя: user_id={user_id}')
|
||
# Создаём нового пользователя
|
||
user = User(
|
||
user_id=user_id,
|
||
username=message.from_user.username,
|
||
first_name=message.from_user.first_name,
|
||
last_name=message.from_user.last_name,
|
||
apartment=apartment_upper,
|
||
)
|
||
session.add(user)
|
||
else:
|
||
logger.info(f'Пользователь найден: verified={user.verified}, apartment={user.apartment}')
|
||
# Если уже верифицирован
|
||
if user.verified:
|
||
await message.answer(
|
||
f'✅ Вы уже верифицированы (кв. {user.apartment})\n\n'
|
||
'Если нужно изменить квартиру, обратитесь к администратору.',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
# Обновляем квартиру
|
||
user.apartment = apartment_upper
|
||
|
||
# Создаём заявку на верификацию (если ещё нет активной)
|
||
stmt = select(VerificationRequest).where(
|
||
VerificationRequest.user_id == user_id,
|
||
VerificationRequest.status == 'pending'
|
||
)
|
||
result = await session.execute(stmt)
|
||
existing_request = result.scalar_one_or_none()
|
||
|
||
if not existing_request:
|
||
logger.info(f'Создаём заявку на верификацию')
|
||
verification = VerificationRequest(
|
||
user_id=user_id,
|
||
apartment=apartment_upper,
|
||
)
|
||
session.add(verification)
|
||
|
||
await session.commit()
|
||
logger.info(f'Верификация сохранена в БД')
|
||
|
||
# Уведомляем пользователя
|
||
await message.answer(
|
||
f'✅ <b>Заявка на верификацию создана!</b>\n\n'
|
||
f'Квартира: <code>{apartment_upper}</code>\n'
|
||
f'Статус: На проверке\n\n'
|
||
f'Администратор проверит данные в ближайшее время.\n'
|
||
f'После подтверждения вы получите доступ ко всем функциям.',
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
# Уведомляем админа
|
||
try:
|
||
user_name = message.from_user.first_name
|
||
username = f'@{message.from_user.username}' if message.from_user.username else 'нет'
|
||
|
||
await message.bot.send_message(
|
||
config.ADMIN_USER_ID,
|
||
f'🔔 <b>Новая заявка на верификацию</b>\n\n'
|
||
f'Жилец: {user_name} {username}\n'
|
||
f'Квартира: {apartment_upper}\n'
|
||
f'ID: <code>{user_id}</code>\n\n'
|
||
f'Для подтверждения используйте:\n'
|
||
f'<code>/verify_approve {user_id}</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
logger.info(f'Админ уведомлён')
|
||
except Exception as e:
|
||
logger.error(f'Не удалось уведомить админа: {e}')
|
||
|
||
except Exception as e:
|
||
logger.error(f'Ошибка при верификации: {e}', exc_info=True)
|
||
await message.answer('❌ Произошла ошибка при верификации. Попробуйте позже.')
|
||
|
||
|
||
@router.message(Command('verify'))
|
||
@admin_only
|
||
async def cmd_verify_list(message: Message):
|
||
"""Список заявок на верификацию (админ)"""
|
||
logger.info(f'Админ запросил список заявок')
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = (
|
||
select(VerificationRequest)
|
||
.where(VerificationRequest.status == 'pending')
|
||
.order_by(VerificationRequest.created_at)
|
||
)
|
||
result = await session.execute(stmt)
|
||
requests = list(result.scalars().all())
|
||
|
||
if not requests:
|
||
await message.answer('✅ Все заявки обработаны')
|
||
return
|
||
|
||
text = f'📋 <b>Заявки на верификацию</b> ({len(requests)}):\n\n'
|
||
|
||
for req in requests:
|
||
# Получаем имя пользователя
|
||
stmt = select(User).where(User.user_id == req.user_id)
|
||
result = await session.execute(stmt)
|
||
user = result.scalar_one_or_none()
|
||
name = user.full_name if user else f'user{req.user_id}'
|
||
|
||
text += f'• {name} — кв. {req.apartment}\n'
|
||
text += f' ID: <code>{req.user_id}</code>\n\n'
|
||
|
||
text += 'Для подтверждения используйте /verify_approve <ID>'
|
||
|
||
await message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.message(Command('verify_approve'))
|
||
@admin_only
|
||
async def cmd_verify_approve(message: Message):
|
||
"""Подтверждение верификации (работает и в личке, и в группе)"""
|
||
logger.info(f'Админ подтверждает верификацию в чате типа: {message.chat.type}')
|
||
|
||
# Парсим ID пользователя из команды
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer('Использование: /verify_approve <user_id>')
|
||
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.verified = True
|
||
user.verification_date = datetime.utcnow()
|
||
|
||
# Обновляем заявку
|
||
stmt = select(VerificationRequest).where(
|
||
VerificationRequest.user_id == user_id,
|
||
VerificationRequest.status == 'pending'
|
||
)
|
||
result = await session.execute(stmt)
|
||
request = result.scalar_one_or_none()
|
||
|
||
if request:
|
||
request.status = 'approved'
|
||
request.reviewed_at = datetime.utcnow()
|
||
request.reviewed_by = message.from_user.id
|
||
|
||
await session.commit()
|
||
|
||
logger.info(f'Пользователь {user_id} верифицирован')
|
||
|
||
# Уведомляем пользователя (в личку)
|
||
try:
|
||
await message.bot.send_message(
|
||
user_id,
|
||
f'✅ <b>Верификация подтверждена!</b>\n\n'
|
||
f'Квартира: {user.apartment}\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'ID: <code>{user_id}</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.message(Command('verify_reject'))
|
||
@admin_only
|
||
async def cmd_verify_reject(message: Message):
|
||
"""Отклонение верификации"""
|
||
args = message.text.split(maxsplit=2)
|
||
if len(args) < 2:
|
||
await message.answer('Использование: /verify_reject <user_id> [причина]')
|
||
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
|
||
|
||
# Обновляем заявку
|
||
stmt = select(VerificationRequest).where(
|
||
VerificationRequest.user_id == user_id,
|
||
VerificationRequest.status == 'pending'
|
||
)
|
||
result = await session.execute(stmt)
|
||
request = result.scalar_one_or_none()
|
||
|
||
if request:
|
||
request.status = 'rejected'
|
||
request.reviewed_at = datetime.utcnow()
|
||
request.reviewed_by = message.from_user.id
|
||
request.reject_reason = reason
|
||
await session.commit()
|
||
|
||
# Уведомляем пользователя
|
||
try:
|
||
await message.bot.send_message(
|
||
user_id,
|
||
f'❌ <b>Верификация отклонена</b>\n\n'
|
||
f'Причина: {reason}\n\n'
|
||
f'Если вы считаете это ошибкой, обратитесь к администратору.'
|
||
)
|
||
except Exception as e:
|
||
logger.error(f'Не удалось уведомить пользователя: {e}')
|
||
|
||
await message.answer(f'❌ Заявка пользователя отклонена')
|