✅ Реализовано: - Добавлено поле image_path в Service модель - Миграция БД выполнена - Бот отправляет картинки с телефонами - Если картинки нет → отправляет текстом - Папка: data/service_images/ 📝 Скрипты: - database/update_service_image.py — загрузка картинок - database/migrate_add_service_images.py — миграция 🎨 Как использовать: 1. Положи картинку в data/service_images/ 2. venv/bin/python3 database/update_service_image.py police img.jpg 3. Проверь в боте: /start → 👮 Участковый 💡 Работает для всех категорий: - police (полиция) - emergency (МЧС) - utility (ЖКХ) - masters (мастера) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
612 lines
22 KiB
Python
612 lines
22 KiB
Python
"""
|
||
Пользовательские команды
|
||
Доступны всем жильцам
|
||
"""
|
||
import logging
|
||
import json
|
||
from aiogram import Router, F
|
||
from aiogram.types import Message, CallbackQuery
|
||
from aiogram.filters import Command, CommandStart
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select
|
||
from database.db import AsyncSessionLocal
|
||
from database.models import User, Service, Announcement
|
||
from keyboards.inline import get_main_menu, get_phones_menu
|
||
from utils.formatters import format_phone_list
|
||
import config
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = Router()
|
||
|
||
|
||
@router.message(CommandStart())
|
||
async def cmd_start(message: Message):
|
||
"""Команда /start"""
|
||
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'🏠 <b>Добро пожаловать, {message.from_user.first_name}!</b>\n\n'
|
||
f'Я — домовой бот нашего дома.\n\n'
|
||
f'❗ <b>Важно:</b> Для полного доступа пройдите верификацию.\n'
|
||
f'Отправьте мне номер вашей квартиры (например: <code>45</code> или <code>12А</code>)'
|
||
)
|
||
await message.answer(welcome_text, parse_mode='HTML')
|
||
else:
|
||
# Обновляем имя
|
||
user.username = message.from_user.username
|
||
user.first_name = message.from_user.first_name
|
||
user.last_name = message.from_user.last_name
|
||
await session.commit()
|
||
|
||
if user.verified:
|
||
await message.answer(
|
||
f'👋 Здравствуйте, {message.from_user.first_name}!\n\n'
|
||
f'Вы верифицированы (кв. {user.apartment}).\n'
|
||
f'Выберите действие:',
|
||
reply_markup=get_main_menu(),
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
await message.answer(
|
||
f'👋 Здравствуйте, {message.from_user.first_name}!\n\n'
|
||
f'⏳ Вы ещё не верифицированы.\n'
|
||
f'Отправьте номер квартиры для верификации.',
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.message(Command('помощь'))
|
||
@router.message(Command('help'))
|
||
async def cmd_help(message: Message):
|
||
"""Справка по командам"""
|
||
text = '''<b>📱 Команды бота:</b>
|
||
|
||
<b>Для всех жильцов:</b>
|
||
📞 /телефон — Экстренные службы
|
||
👷 /мастера — Сантехники, электрики
|
||
👮 /участковый — Контакты полиции
|
||
📅 /график — Отключения воды/света
|
||
📢 /объявления — Новости дома
|
||
🔍 /моя_квартира — Проверка привязки
|
||
|
||
<b>Верификация:</b>
|
||
Просто отправьте номер квартиры текстом
|
||
|
||
<b>Администратору:</b>
|
||
/admin — Панель админа
|
||
/stats — Статистика чата
|
||
/spies — Подозрительные
|
||
/export — Экспорт JSON
|
||
/users — Список жильцов'''
|
||
|
||
await message.answer(text, parse_mode='HTML', reply_markup=get_main_menu())
|
||
|
||
|
||
@router.callback_query(F.data == 'help')
|
||
async def cb_help(callback: CallbackQuery):
|
||
"""Справка (callback)"""
|
||
await callback.answer()
|
||
text = '''<b>🏠 Домовой бот — помощь</b>
|
||
|
||
Этот бот создан для помощи жильцам нашего дома.
|
||
|
||
<b>📞 Экстренные телефоны</b>
|
||
Быстрый доступ к телефонам служб
|
||
|
||
<b>👷 Мастера</b>
|
||
Проверенные сантехники и электрики
|
||
|
||
<b>🔍 Моя квартира</b>
|
||
Проверить привязку к квартире
|
||
|
||
<b>⏳ Верификация</b>
|
||
Отправьте номер квартиры текстом'''
|
||
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data == 'phones')
|
||
async def cb_phones(callback: CallbackQuery):
|
||
"""Телефоны — главное меню"""
|
||
await callback.answer()
|
||
await callback.message.answer(
|
||
'📞 Выберите категорию:',
|
||
reply_markup=get_phones_menu()
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data == 'phones_emergency')
|
||
async def cb_phones_emergency(callback: CallbackQuery):
|
||
"""Экстренные телефоны"""
|
||
await callback.answer()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(Service).where(Service.category == 'emergency').order_by(Service.name)
|
||
result = await session.execute(stmt)
|
||
services = list(result.scalars().all())
|
||
|
||
text = '<b>🚨 Экстренные службы</b>\n\n'
|
||
text += format_phone_list([
|
||
{
|
||
'name': s.name,
|
||
'phone': s.phone,
|
||
'description': s.description,
|
||
'category': s.category
|
||
}
|
||
for s in services
|
||
])
|
||
|
||
if not services:
|
||
text += 'Список пуст. Администратор, добавьте телефоны в БД.'
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
return
|
||
|
||
# Если есть картинка у первой службы — отправляем с фото
|
||
first_service = services[0]
|
||
if first_service.image_path:
|
||
from aiogram.types import FSInputFile
|
||
try:
|
||
photo = FSInputFile(first_service.image_path)
|
||
await callback.message.answer_photo(photo, text, parse_mode='HTML')
|
||
return
|
||
except Exception as e:
|
||
logger.error(f"Ошибка отправки фото: {e}")
|
||
|
||
# Если нет картинки — отправляем текстом
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data == 'phones_utility')
|
||
async def cb_phones_utility(callback: CallbackQuery):
|
||
"""ЖКХ службы"""
|
||
await callback.answer()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(Service).where(Service.category == 'utility').order_by(Service.name)
|
||
result = await session.execute(stmt)
|
||
services = list(result.scalars().all())
|
||
|
||
text = '<b>🔧 Службы ЖКХ</b>\n\n'
|
||
text += format_phone_list([
|
||
{
|
||
'name': s.name,
|
||
'phone': s.phone,
|
||
'description': s.description,
|
||
'category': s.category
|
||
}
|
||
for s in services
|
||
])
|
||
|
||
if not services:
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
return
|
||
|
||
# Если есть картинка — отправляем с фото
|
||
first_service = services[0]
|
||
if first_service.image_path:
|
||
from aiogram.types import FSInputFile
|
||
try:
|
||
photo = FSInputFile(first_service.image_path)
|
||
await callback.message.answer_photo(photo, text, parse_mode='HTML')
|
||
return
|
||
except Exception as e:
|
||
logger.error(f"Ошибка отправки фото: {e}")
|
||
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data == 'phones_police')
|
||
async def cb_phones_police(callback: CallbackQuery):
|
||
"""Полиция"""
|
||
await callback.answer()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(Service).where(Service.category == 'police').order_by(Service.name)
|
||
result = await session.execute(stmt)
|
||
services = list(result.scalars().all())
|
||
|
||
text = '<b>👮 Полиция</b>\n\n'
|
||
text += format_phone_list([
|
||
{
|
||
'name': s.name,
|
||
'phone': s.phone,
|
||
'description': s.description,
|
||
'category': s.category
|
||
}
|
||
for s in services
|
||
])
|
||
|
||
if not services:
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
return
|
||
|
||
# Если есть картинка — отправляем с фото
|
||
first_service = services[0]
|
||
if first_service.image_path:
|
||
from aiogram.types import FSInputFile
|
||
try:
|
||
photo = FSInputFile(first_service.image_path)
|
||
await callback.message.answer_photo(photo, text, parse_mode='HTML')
|
||
return
|
||
except Exception as e:
|
||
logger.error(f"Ошибка отправки фото: {e}")
|
||
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data == 'masters')
|
||
async def cb_masters(callback: CallbackQuery):
|
||
"""Мастера"""
|
||
await callback.answer()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(Service).where(Service.category.in_(['masters', 'plumber', 'electrician'])).order_by(Service.name)
|
||
result = await session.execute(stmt)
|
||
services = list(result.scalars().all())
|
||
|
||
text = '<b>👷 Проверенные мастера</b>\n\n'
|
||
text += format_phone_list([
|
||
{
|
||
'name': s.name,
|
||
'phone': s.phone,
|
||
'description': s.description,
|
||
'category': s.category,
|
||
'rating': s.rating
|
||
}
|
||
for s in services
|
||
])
|
||
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data == 'my_apartment')
|
||
async def cb_my_apartment(callback: CallbackQuery):
|
||
"""Моя квартира"""
|
||
await callback.answer()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(User).where(User.user_id == callback.from_user.id)
|
||
result = await session.execute(stmt)
|
||
user = result.scalar_one_or_none()
|
||
|
||
if not user:
|
||
text = '❌ Вы не зарегистрированы. Нажмите /start'
|
||
elif not user.apartment:
|
||
text = f'⏳ Вы не верифицированы.\n\nОтправьте номер квартиры текстом.'
|
||
else:
|
||
status = '✅' if user.verified else '⏳'
|
||
text = (
|
||
f'{status} <b>Ваша квартира: {user.apartment}</b>\n\n'
|
||
f'Статус: {"Верифицирован" if user.verified else "Не верифицирован"}\n'
|
||
f'Сообщений: {user.message_count}'
|
||
)
|
||
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data == 'schedule')
|
||
async def cb_schedule(callback: CallbackQuery):
|
||
"""График отключений"""
|
||
await callback.answer()
|
||
|
||
# TODO: Загрузить реальный график
|
||
text = '''<b>📅 График отключений</b>
|
||
|
||
⚠️ Информация обновляется
|
||
|
||
Вода:
|
||
• Плановое отключение: каждое второе воскресенье месяца, 9:00-17:00
|
||
|
||
Электричество:
|
||
• Без плановых отключений
|
||
|
||
Отопление:
|
||
• Сезон: октябрь — апрель
|
||
|
||
Для актуальной информации обратитесь в УК.'''
|
||
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.callback_query(F.data == 'main_menu')
|
||
async def cb_main_menu(callback: CallbackQuery):
|
||
"""Главное меню"""
|
||
await callback.answer()
|
||
await callback.message.answer(
|
||
'🏠 <b>Главное меню</b>',
|
||
reply_markup=get_main_menu(),
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
# Текстовые команды (алиасы)
|
||
@router.message(F.text.lower() == 'телефоны')
|
||
@router.message(F.text.lower() == 'телефон')
|
||
async def msg_phones_text(message: Message):
|
||
"""Текстовая команда телефоны"""
|
||
await message.answer(
|
||
'📞 Выберите категорию:',
|
||
reply_markup=get_phones_menu()
|
||
)
|
||
|
||
|
||
@router.message(F.text.lower() == 'мастера')
|
||
async def msg_masters_text(message: Message):
|
||
"""Текстовая команда мастера"""
|
||
await message.answer(
|
||
'👷 Загружаю список мастеров...',
|
||
reply_markup=get_main_menu()
|
||
)
|
||
# Перенаправляем на callback
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(Service).where(Service.category.in_(['masters', 'plumber', 'electrician']))
|
||
result = await session.execute(stmt)
|
||
services = list(result.scalars().all())
|
||
|
||
text = '<b>👷 Проверенные мастера</b>\n\n'
|
||
text += format_phone_list([
|
||
{'name': s.name, 'phone': s.phone, 'description': s.description, 'rating': s.rating}
|
||
for s in services
|
||
])
|
||
await message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.message(F.text.lower() == 'помощь')
|
||
async def msg_help_text(message: Message):
|
||
"""Текстовая команда помощь"""
|
||
await cmd_help(message)
|
||
|
||
|
||
# =============================================================================
|
||
# ОБРАБОТЧИКИ НОВЫХ КНОПОК МЕНЮ
|
||
# =============================================================================
|
||
|
||
from keyboards.inline import get_thank_user_keyboard, get_thank_confirm_keyboard
|
||
from sqlalchemy import desc
|
||
|
||
|
||
# Хранилище страниц для благодарностей
|
||
_thank_pages: dict[int, dict] = {}
|
||
|
||
|
||
@router.callback_query(F.data == 'achievements')
|
||
async def cb_achievements_menu(callback: CallbackQuery):
|
||
"""Кнопка Достижения"""
|
||
await callback.answer()
|
||
# Перенаправляем на команду /achievements
|
||
from handlers.achievements import cmd_achievements
|
||
await cmd_achievements(callback.message)
|
||
|
||
|
||
@router.callback_query(F.data == 'events')
|
||
async def cb_events_menu(callback: CallbackQuery):
|
||
"""Кнопка События"""
|
||
await callback.answer()
|
||
# Перенаправляем на команду /events
|
||
from handlers.events import cmd_events
|
||
await cmd_events(callback.message)
|
||
|
||
|
||
@router.callback_query(F.data == 'payments')
|
||
async def cb_payments_menu(callback: CallbackQuery):
|
||
"""Кнопка Платежи"""
|
||
await callback.answer()
|
||
# Перенаправляем на команду /payments
|
||
from handlers.payments import cmd_payments
|
||
await cmd_payments(callback.message)
|
||
|
||
|
||
@router.callback_query(F.data == 'polls')
|
||
async def cb_polls_menu(callback: CallbackQuery):
|
||
"""Кнопка Опросы"""
|
||
await callback.answer()
|
||
# Перенаправляем на команду /polls
|
||
from handlers.polls import cmd_polls
|
||
await cmd_polls(callback.message)
|
||
|
||
|
||
@router.callback_query(F.data == 'thank_user')
|
||
async def cb_thank_user_menu(callback: CallbackQuery):
|
||
"""Кнопка Поблагодарить - выбор пользователя"""
|
||
await callback.answer()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
# Показываем активных пользователей (по количеству сообщений)
|
||
stmt = select(User).order_by(desc(User.message_count)).limit(100)
|
||
result = await session.execute(stmt)
|
||
users = list(result.scalars().all())
|
||
|
||
if not users:
|
||
await callback.message.answer('📭 Пользователей нет')
|
||
return
|
||
|
||
# Сохраняем список для сессии
|
||
_thank_pages[callback.from_user.id] = {
|
||
'users': users,
|
||
'page': 0
|
||
}
|
||
|
||
await callback.message.answer(
|
||
'🙏 <b>Поблагодарить пользователя</b>\n\n'
|
||
'Выберите пользователя которому хотите сказать спасибо:\n'
|
||
'(+10 к рейтингу)',
|
||
reply_markup=get_thank_user_keyboard(users, page=0),
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data.startswith('thank_page_'))
|
||
async def cb_thank_page(callback: CallbackQuery):
|
||
"""Переключение страницы в списке для благодарности"""
|
||
await callback.answer()
|
||
|
||
page = int(callback.data.split('_')[-1])
|
||
user_data = _thank_pages.get(callback.from_user.id)
|
||
|
||
if not user_data:
|
||
await callback.message.answer('❌ Сессия истекла. Выберите "Поблагодарить" заново')
|
||
return
|
||
|
||
users = user_data['users']
|
||
user_data['page'] = page
|
||
|
||
await callback.message.edit_reply_markup(
|
||
reply_markup=get_thank_user_keyboard(users, page=page)
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data.startswith('thank_select_'))
|
||
async def cb_thank_select(callback: CallbackQuery):
|
||
"""Выбор конкретного пользователя для благодарности"""
|
||
await callback.answer()
|
||
|
||
user_id = int(callback.data.split('_')[-1])
|
||
|
||
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 callback.message.answer('❌ Пользователь не найден')
|
||
return
|
||
|
||
name = user.full_name or f'user{user_id}'
|
||
apt = f' (кв. {user.apartment})' if user.apartment else ''
|
||
|
||
await callback.message.answer(
|
||
f'🙏 <b>Поблагодарить пользователя?</b>\n\n'
|
||
f'{name}{apt}\n\n'
|
||
f'Пользователь получит +10 к рейтингу.',
|
||
reply_markup=get_thank_confirm_keyboard(user_id),
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data.startswith('thank_confirm_'))
|
||
async def cb_thank_confirm(callback: CallbackQuery):
|
||
"""Подтверждение благодарности"""
|
||
await callback.answer()
|
||
|
||
target_user_id = int(callback.data.split('_')[-1])
|
||
from_user_id = callback.from_user.id
|
||
|
||
# Проверяем что не благодарит сам себя
|
||
if target_user_id == from_user_id:
|
||
await callback.message.answer('❌ Нельзя поблагодарить самого себя!')
|
||
return
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
# Находим того кого благодарят
|
||
stmt = select(User).where(User.user_id == target_user_id)
|
||
result = await session.execute(stmt)
|
||
target_user = result.scalar_one_or_none()
|
||
|
||
# Находим того кто благодарит
|
||
stmt = select(User).where(User.user_id == from_user_id)
|
||
result = await session.execute(stmt)
|
||
from_user = result.scalar_one_or_none()
|
||
|
||
if not target_user or not from_user:
|
||
await callback.message.answer('❌ Пользователь не найден')
|
||
return
|
||
|
||
# Проверяли ли уже сегодня
|
||
from datetime import datetime, timedelta
|
||
from database.models import Thanks
|
||
|
||
today = datetime.utcnow().date()
|
||
stmt = select(Thanks).where(
|
||
Thanks.from_user_id == from_user_id,
|
||
Thanks.created_at >= datetime.combine(today, datetime.min.time())
|
||
)
|
||
result = await session.execute(stmt)
|
||
existing = result.scalar_one_or_none()
|
||
|
||
if existing:
|
||
await callback.message.answer('ℹ️ Вы уже благодарили кого-то сегодня. Можно раз в сутки.')
|
||
return
|
||
|
||
# Создаём благодарность
|
||
thanks = Thanks(
|
||
from_user_id=from_user_id,
|
||
to_user_id=target_user_id,
|
||
message='Благодарность через меню',
|
||
)
|
||
session.add(thanks)
|
||
|
||
# +10 к рейтингу
|
||
target_user.rating += 10
|
||
|
||
await session.commit()
|
||
|
||
name = target_user.full_name or f'user{target_user_id}'
|
||
|
||
await callback.message.answer(
|
||
f'✅ <b>Спасибо отправлено!</b>\n\n'
|
||
f'{name} получил +10 к рейтингу.\n'
|
||
f'Вы можете поблагодарить ещё раз через 24 часа.',
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.callback_query(F.data == 'announcements')
|
||
async def cb_announcements_menu(callback: CallbackQuery):
|
||
"""Кнопка Объявления"""
|
||
await callback.answer()
|
||
# Перенаправляем на команду /ads
|
||
from handlers.ads import cmd_ads
|
||
await cmd_ads(callback.message)
|
||
|
||
|
||
@router.callback_query(F.data == 'schedule')
|
||
async def cb_schedule_menu(callback: CallbackQuery):
|
||
"""Кнопка График отключений"""
|
||
await callback.answer()
|
||
# Перенаправляем на команду /график
|
||
from handlers.schedule import cmd_schedule
|
||
await cmd_schedule(callback.message)
|
||
|
||
|
||
@router.callback_query(F.data == 'my_apartment')
|
||
async def cb_my_apartment_menu(callback: CallbackQuery):
|
||
"""Кнопка Моя квартира"""
|
||
await callback.answer()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = select(User).where(User.user_id == callback.from_user.id)
|
||
result = await session.execute(stmt)
|
||
user = result.scalar_one_or_none()
|
||
|
||
if not user:
|
||
await callback.message.answer('❌ Вы не зарегистрированы')
|
||
return
|
||
|
||
apt = user.apartment or '❓ Не указана'
|
||
verified = '✅' if user.verified else '⏳'
|
||
|
||
text = f'🏠 <b>Ваша квартира</b>\n\n'
|
||
text += f'Номер: <code>{apt}</code>\n'
|
||
text += f'Статус: {verified} {"Верифицирован" if user.verified else "Не верифицирован"}\n'
|
||
text += f'Рейтинг: {user.rating}\n'
|
||
text += f'Уровень: {user.level}\n'
|
||
|
||
await callback.message.answer(text, parse_mode='HTML')
|