"""
Пользовательские команды 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()
@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"
"Добро пожаловать в цифровую экосистему ЛКМ37.\n"
"Я твой проводник и помощник по дому.\n\n"
"🛡️ Для доступа ко всем функциям нажми кнопку верификации в меню.")
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 callback.message.edit_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.message(Command('phones'))
@router.callback_query(F.data == 'phones')
async def cb_phones(event):
text = '📞 ЭКСТРЕННЫЕ ТЕЛЕФОНЫ\n\nВыберите категорию ниже:'
kb = get_phones_menu()
if isinstance(event, CallbackQuery):
await event.answer()
await event.message.edit_text(text, reply_markup=kb, parse_mode='HTML')
else:
await event.answer(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("🔒 Доступ ограничен\n\nМой юридический ИИ-модуль доступен только участникам Инициативной группы и по персональному разрешению администратора.\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())
if not services:
return await callback.message.edit_text("👷 Данные для этой категории еще не заполнены.", reply_markup=get_main_menu())
text = f"📞 СПИСОК ТЕЛЕФОНОВ: {cat_code.upper()}\n\n"
for s in services:
text += f"🔹 {s.name}\n └ 📱 {s.phone}\n"
if s.description: text += f" └ 📝 {s.description}\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=get_main_menu(), parse_mode='HTML')
else:
await callback.message.edit_text(text, reply_markup=get_main_menu(), parse_mode='HTML')
await callback.answer()
@router.message(Command('rules'))
@router.callback_query(F.data == 'rules')
async def cb_rules(event):
text = "📜 ПРАВИЛА ЧАТА ЛКМ37\n\n1. Уважение к соседям\n2. Никакого спама и рекламы\n3. Мат запрещен\n4. Обсуждаем только дела дома."
kb = InlineKeyboardBuilder()
kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="main_menu"))
if isinstance(event, Message):
await event.answer(text, reply_markup=kb.as_markup(), parse_mode='HTML')
else:
await event.answer()
await event.message.edit_text(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 = ("🏢 О НАШЕМ ДОМЕ (ЛКМ37)\n\n"
"📍 Адрес: пр-т Ленинского Комсомола, д. 37\n"
"🏗️ Тип дома: Многоквартирный жилой дом\n"
"🛠️ Управляющая компания: (укажите вашу УК)\n\n"
"Этот бот создан для объединения жильцов и быстрого решения проблем.")
kb = InlineKeyboardBuilder()
kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="main_menu"))
if isinstance(event, Message):
await event.answer(text, reply_markup=kb.as_markup(), parse_mode='HTML')
else:
await event.answer()
await event.message.edit_text(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"👤 ТВОЙ ПРОФИЛЬ\n\n"
f"ID: {user_id}\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)
if isinstance(event, Message):
await event.answer(text, reply_markup=kb, parse_mode='HTML')
else:
await event.answer()
await event.message.edit_text(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"👤 ТВОЙ ПРОФИЛЬ\n\n"
f"ID: {user_id}\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 callback.message.edit_text(text, reply_markup=kb, parse_mode='HTML')
@router.callback_query(F.data == "edit_location")
async def edit_location_start(callback: CallbackQuery):
await callback.message.edit_text("Введите ваш подъезд и этаж через пробел\nНапример: '3 5' (3 подъезд, 5 этаж)", parse_mode='HTML')
await callback.answer()
@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 = ("❓ СПРАВКА\n\n"
"• Используйте кнопки в главном меню для навигации.\n"
"• Если бот не понимает команду, просто напишите вопрос текстом — наш ИИ постарается помочь.\n\n"
"Список команд:\n"
"/start — Главное меню\n"
"/phones — Телефоны\n"
"/about — О доме\n"
"/profile — Твой профиль\n"
"/rules — Правила чата")
kb = InlineKeyboardBuilder()
kb.row(InlineKeyboardButton(text="🏠 В меню", callback_data="main_menu"))
if isinstance(event, Message):
await event.answer(text, reply_markup=kb.as_markup(), parse_mode='HTML')
else:
await event.answer()
await event.message.edit_text(text, reply_markup=kb.as_markup(), parse_mode='HTML')