""" Пользовательские команды 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" "Добро пожаловать в цифровую экосистему ЛКМ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 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 = '📞 ЭКСТРЕННЫЕ ТЕЛЕФОНЫ\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("🔒 Доступ ограничен\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()) 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"📞 СПИСОК ТЕЛЕФОНОВ: {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=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 = "📜 ПРАВИЛА ЧАТА ЛКМ37\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 = ("🏢 О НАШЕМ ДОМЕ (ЛКМ37)\n\n" "📍 Адрес: пр-т Ленинского Комсомола, д. 37\n" "🏗️ Тип дома: Многоквартирный жилой дом\n" "🛠️ Управляющая компания: (укажите вашу УК)\n\n" "Этот бот создан для объединения жильцов и быстрого решения проблем.") 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"👤 ТВОЙ ПРОФИЛЬ\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) 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"👤 ТВОЙ ПРОФИЛЬ\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 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Например: '3 5' (3 подъезд, 5 этаж)") @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")) await safe_edit_or_reply(event, text, reply_markup=kb.as_markup(), parse_mode='HTML')