251 lines
13 KiB
Python
251 lines
13 KiB
Python
"""
|
||
Пользовательские команды 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"
|
||
"Добро пожаловать в цифровую экосистему <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 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 = '📞 <b>ЭКСТРЕННЫЕ ТЕЛЕФОНЫ</b>\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("🔒 <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())
|
||
|
||
if not services:
|
||
return await callback.message.edit_text("👷 Данные для этой категории еще не заполнены.", reply_markup=get_main_menu())
|
||
|
||
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=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 = "<b>📜 ПРАВИЛА ЧАТА ЛКМ37</b>\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 = ("<b>🏢 О НАШЕМ ДОМЕ (ЛКМ37)</b>\n\n"
|
||
"📍 Адрес: пр-т Ленинского Комсомола, д. 37\n"
|
||
"🏗️ Тип дома: Многоквартирный жилой дом\n"
|
||
"🛠️ Управляющая компания: (укажите вашу УК)\n\n"
|
||
"<i>Этот бот создан для объединения жильцов и быстрого решения проблем.</i>")
|
||
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"👤 <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()
|
||
|
||
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 == "edit_location")
|
||
async def edit_location_start(callback: CallbackQuery):
|
||
await callback.message.edit_text("Введите ваш подъезд и этаж через пробел\n<i>Например: '3 5' (3 подъезд, 5 этаж)</i>", 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()
|
||
await message.answer(f"✅ Данные обновлены: Подъезд {staircase}, Этаж {floor}", reply_markup=get_profile_keyboard(), 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"))
|
||
|
||
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')
|