domovoy_bot/handlers/users.py

304 lines
16 KiB
Python
Raw 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.

"""
Пользовательские команды LKM37 REBORN
Доступны всем жильцам.
"""
import logging
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.filters import Command, CommandStart, StateFilter
from aiogram.fsm.context import FSMContext
from sqlalchemy import select, update
from database.db import AsyncSessionLocal
from database.models import User, InitiativeGroup
from keyboards.inline import get_main_menu, get_phones_menu, get_profile_keyboard
from services.ai_service import ai_service
import config
logger = logging.getLogger(__name__)
router = Router()
async def safe_edit_or_reply(event, text: str, reply_markup=None, parse_mode='HTML'):
"""
Безопасное редактирование сообщения.
Если сообщение содержит медиа (фото и т.д.), мы удаляем его и отправляем новое.
"""
if isinstance(event, CallbackQuery):
await event.answer()
message = event.message
if (hasattr(message, 'photo') and message.photo) or \
(hasattr(message, 'video') and message.video) or \
(hasattr(message, 'document') and message.document) or \
(hasattr(message, 'content_type') and message.content_type != 'text'):
try:
await message.delete()
except Exception as e:
logger.warning(f"Не удалось удалить сообщение с медиа: {e}")
await message.answer(text, reply_markup=reply_markup, parse_mode=parse_mode)
else:
try:
await message.edit_text(text, reply_markup=reply_markup, parse_mode=parse_mode)
except Exception as e:
logger.warning(f"Ошибка edit_text: {e}. Отправляем новое сообщение.")
await message.answer(text, reply_markup=reply_markup, parse_mode=parse_mode)
elif isinstance(event, Message):
await event.answer(text, reply_markup=reply_markup, parse_mode=parse_mode)
@router.message(Command('cancel'), StateFilter('*'))
@router.message(F.text.lower().in_(['отмена', 'отменить']), StateFilter('*'))
async def global_cancel(message: Message, state: FSMContext):
"""Глобальный выход из любого состояния FSM"""
current_state = await state.get_state()
if current_state:
await state.clear()
await message.answer("❌ Действие отменено.", reply_markup=get_main_menu())
else:
await message.answer(" Нет активных действий для отмены.", reply_markup=get_main_menu())
@router.message(CommandStart())
async def cmd_start(message: Message):
"""Команда /start"""
from utils.ui_styles import fmt_msg, get_level_badge, get_user_level_key
if message.chat.type != 'private': return
user_id = message.from_user.id
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:
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)
session.add(user)
await session.commit()
welcome_text = (f"{fmt_msg('Инициализация протокола связи...', 'system')}\n"
f"{fmt_msg('Обнаружен новый субъект.', 'system')}\n\n"
"Добро пожаловать в цифровую экосистему <b>ЛКМ37</b>.\n"
"Я твой проводник и помощник по дому.\n\n"
"🛡️ <b>Для доступа ко всем функциям нажми кнопку верификации в меню.</b>")
is_ig = False
else:
ig_stmt = select(InitiativeGroup).where(InitiativeGroup.user_id == user_id, InitiativeGroup.is_active == True)
is_ig = (await session.execute(ig_stmt)).scalar_one_or_none() is not None
level_key = get_user_level_key(user_id, user.is_admin, user.verified, is_ig)
welcome_text = (f"{fmt_msg('Доступ разрешен.', 'system')}\n"
f"{fmt_msg(f'Статус: {get_level_badge(level_key)}', 'system')}\n\n"
f"Рад видеть тебя снова, {message.from_user.first_name}!\n"
"Чем могу помочь сегодня?")
await message.answer(welcome_text, reply_markup=get_main_menu(verified=user.verified, is_ig=is_ig, is_admin=(user.is_admin or user_id == config.ADMIN_USER_ID), is_superadmin=(user_id == config.ADMIN_USER_ID)), parse_mode='HTML')
@router.callback_query(F.data == 'main_menu')
async def cb_main_menu(callback: CallbackQuery):
"""Возврат в главное меню"""
user_id = callback.from_user.id
await callback.answer()
async with AsyncSessionLocal() as session:
stmt = select(User).where(User.user_id == user_id)
user = (await session.execute(stmt)).scalar_one_or_none()
ig_stmt = select(InitiativeGroup).where(InitiativeGroup.user_id == user_id, InitiativeGroup.is_active == True)
is_ig = (await session.execute(ig_stmt)).scalar_one_or_none() is not None
await safe_edit_or_reply(
callback,
"Главное меню:",
reply_markup=get_main_menu(verified=user.verified, is_ig=is_ig, is_admin=(user.is_admin or user_id == config.ADMIN_USER_ID), is_superadmin=(user_id == config.ADMIN_USER_ID)),
parse_mode='HTML'
)
@router.message(Command('phones'))
@router.callback_query(F.data == 'phones')
async def cb_phones(event):
text = '📞 <b>ЭКСТРЕННЫЕ ТЕЛЕФОНЫ</b>\n\nВыберите категорию ниже:'
kb = get_phones_menu()
await safe_edit_or_reply(event, text, reply_markup=kb, parse_mode='HTML')
@router.message(F.text & ~F.text.startswith('/'))
async def chat_with_ai(message: Message):
"""Обработка свободного общения с ИИ (только для верифицированных)"""
if message.chat.type != 'private': return
async with AsyncSessionLocal() as session:
stmt = select(User).where(User.user_id == message.from_user.id)
user = (await session.execute(stmt)).scalar_one_or_none()
ig_stmt = select(InitiativeGroup).where(InitiativeGroup.user_id == message.from_user.id, InitiativeGroup.is_active == True)
is_ig = (await session.execute(ig_stmt)).scalar_one_or_none() is not None
# Проверка прав: Админ ИЛИ ИГ ИЛИ персональный доступ
if not (user and (user.is_admin or is_ig or False)):
await message.answer("🔒 <b>Доступ ограничен</b>\n\nМой юридический ИИ-модуль доступен только участникам <b>Инициативной группы</b> и по персональному разрешению администратора.\n\nОбратитесь к @atyrin для получения доступа к базе знаний ГИС ЖКХ.", parse_mode='HTML')
return
user_info = {"apartment": user.apartment, "verified": user.verified, "role": "ИГ" if is_ig else "Жилец"}
# Отправляем индикатор печати
await message.bot.send_chat_action(chat_id=message.chat.id, action="typing")
answer = await ai_service.answer_question(message.text, user_info)
await message.answer(answer, parse_mode='Markdown')
@router.callback_query(F.data.startswith('phones_'))
async def cb_phone_category(callback: CallbackQuery):
"""Выдача телефонов по категориям"""
from database.models import Service
from aiogram.types import FSInputFile
import os
category_map = {
'phones_emergency': 'emergency',
'phones_utility': 'utility',
'phones_police': 'police',
'phones_masters': 'masters'
}
cat_code = category_map.get(callback.data)
if not cat_code: return await callback.answer()
async with AsyncSessionLocal() as session:
stmt = select(Service).where(Service.category == cat_code).order_by(Service.name)
services = list((await session.execute(stmt)).scalars().all())
user_stmt = select(User).where(User.user_id == callback.from_user.id)
user = (await session.execute(user_stmt)).scalar_one_or_none()
ig_stmt = select(InitiativeGroup).where(InitiativeGroup.user_id == callback.from_user.id, InitiativeGroup.is_active == True)
is_ig = (await session.execute(ig_stmt)).scalar_one_or_none() is not None
is_admin = user.is_admin or callback.from_user.id == config.ADMIN_USER_ID if user else False
is_superadmin = callback.from_user.id == config.ADMIN_USER_ID
verified = user.verified if user else False
menu_kb = get_main_menu(verified=verified, is_ig=is_ig, is_admin=is_admin, is_superadmin=is_superadmin)
if not services:
return await safe_edit_or_reply(callback, "👷 Данные для этой категории еще не заполнены.", reply_markup=menu_kb)
text = f"📞 <b>СПИСОК ТЕЛЕФОНОВ: {cat_code.upper()}</b>\n\n"
for s in services:
text += f"🔹 <b>{s.name}</b>\n └ 📱 <code>{s.phone}</code>\n"
if s.description: text += f" └ 📝 <i>{s.description}</i>\n"
text += "\n"
# Если есть фото, придется прислать новое сообщение (edit_text не умеет менять текст на фото)
photo_path = services[0].image_path if services[0].image_path else None
if photo_path and os.path.exists(photo_path):
await callback.message.delete()
await callback.message.answer_photo(FSInputFile(photo_path), caption=text, reply_markup=menu_kb, parse_mode='HTML')
else:
await safe_edit_or_reply(callback, text, reply_markup=menu_kb, parse_mode='HTML')
@router.message(Command('rules'))
@router.callback_query(F.data == 'rules')
async def cb_rules(event):
text = "<b>📜 ПРАВИЛА ЧАТА ЛКМ37</b>\n\n1. Уважение к соседям\n2. Никакого спама и рекламы\n3. Мат запрещен\n4. Обсуждаем только дела дома."
kb = InlineKeyboardBuilder()
kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="main_menu"))
await safe_edit_or_reply(event, text, reply_markup=kb.as_markup(), parse_mode='HTML')
@router.message(Command('about'))
@router.callback_query(F.data == 'about_house')
async def cb_about(event):
text = ("<b>🏢 О НАШЕМ ДОМЕ (ЛКМ37)</b>\n\n"
"📍 Адрес: пр-т Ленинского Комсомола, д. 37\n"
"🏗️ Тип дома: Многоквартирный жилой дом\n"
"🛠️ Управляющая компания: (укажите вашу УК)\n\n"
"<i>Этот бот создан для объединения жильцов и быстрого решения проблем.</i>")
kb = InlineKeyboardBuilder()
kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="main_menu"))
await safe_edit_or_reply(event, text, reply_markup=kb.as_markup(), parse_mode='HTML')
@router.message(Command('profile'))
@router.callback_query(F.data == 'my_profile')
async def cb_profile(event):
from utils.ui_styles import get_level_badge, get_user_level_key
user_id = event.from_user.id
async with AsyncSessionLocal() as session:
user = await session.get(User, user_id)
ig_stmt = select(InitiativeGroup).where(InitiativeGroup.user_id == user_id, InitiativeGroup.is_active == True)
is_ig = (await session.execute(ig_stmt)).scalar_one_or_none() is not None
badge = get_level_badge(get_user_level_key(user_id, user.is_admin, user.verified, is_ig))
text = (f"👤 <b>ТВОЙ ПРОФИЛЬ</b>\n\n"
f"ID: <code>{user_id}</code>\n"
f"Статус: {badge}\n"
f"Квартира: {user.apartment or 'Не указана'}\n"
f"Подъезд: {user.staircase or ''} | Этаж: {user.floor or ''}\n"
f"Рейтинг: {user.rating} 🏆")
kb = get_profile_keyboard(user.meter_reminders_enabled if user else True)
await safe_edit_or_reply(event, text, reply_markup=kb, parse_mode='HTML')
@router.callback_query(F.data == "toggle_meter_reminders")
async def cb_toggle_meter_reminders(callback: CallbackQuery):
user_id = callback.from_user.id
from utils.ui_styles import get_level_badge, get_user_level_key
async with AsyncSessionLocal() as session:
user = await session.get(User, user_id)
if not user:
await callback.answer("Пользователь не найден", show_alert=True)
return
new_val = not user.meter_reminders_enabled
user.meter_reminders_enabled = new_val
await session.commit()
await callback.answer(
f"Напоминания о счетчиках {'включены' if new_val else 'отключены'}!"
)
ig_stmt = select(InitiativeGroup).where(InitiativeGroup.user_id == user_id, InitiativeGroup.is_active == True)
is_ig = (await session.execute(ig_stmt)).scalar_one_or_none() is not None
badge = get_level_badge(get_user_level_key(user_id, user.is_admin, user.verified, is_ig))
text = (f"👤 <b>ТВОЙ ПРОФИЛЬ</b>\n\n"
f"ID: <code>{user_id}</code>\n"
f"Статус: {badge}\n"
f"Квартира: {user.apartment or 'Не указана'}\n"
f"Подъезд: {user.staircase or ''} | Этаж: {user.floor or ''}\n"
f"Рейтинг: {user.rating} 🏆")
kb = get_profile_keyboard(new_val)
await safe_edit_or_reply(callback, text, reply_markup=kb, parse_mode='HTML')
@router.callback_query(F.data == "edit_location")
async def edit_location_start(callback: CallbackQuery):
await safe_edit_or_reply(callback, "Введите ваш подъезд и этаж через пробел\n<i>Например: '3 5' (3 подъезд, 5 этаж)</i>")
@router.message(F.text.regexp(r"^\d+\s+\d+$"))
async def process_location(message: Message):
try:
staircase, floor = map(int, message.text.split())
async with AsyncSessionLocal() as session:
await session.execute(update(User).where(User.user_id == message.from_user.id).values(staircase=staircase, floor=floor))
await session.commit()
user = await session.get(User, message.from_user.id)
meter_enabled = user.meter_reminders_enabled if user else True
await message.answer(f"✅ Данные обновлены: Подъезд {staircase}, Этаж {floor}", reply_markup=get_profile_keyboard(meter_enabled), parse_mode='HTML')
except Exception as e:
await message.answer("❌ Ошибка формата. Введите два числа через пробел.")
@router.message(Command('help'))
@router.callback_query(F.data == 'help')
async def cb_help(event):
text = ("<b>❓ СПРАВКА</b>\n\n"
"• Используйте кнопки в главном меню для навигации.\n"
"• Если бот не понимает команду, просто напишите вопрос текстом — наш ИИ постарается помочь.\n\n"
"<b>Список команд:</b>\n"
"/start — Главное меню\n"
"/phones — Телефоны\n"
"/about — О доме\n"
"/profile — Твой профиль\n"
"/rules — Правила чата")
kb = InlineKeyboardBuilder()
kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="main_menu"))
await safe_edit_or_reply(event, text, reply_markup=kb.as_markup(), parse_mode='HTML')