🐛 ИСПРАВЛЕНИЯ: • ChatMemberFilter → ChatMemberUpdatedFilter • Исправлен импорт в handlers/users.py • Бот падал с ImportError ✅ ПРОВЕРЕНО: • python3 -m py_compile — OK • systemctl restart domovoy-bot — OK • Active: active (running) • Memory: 132MB — норма 📁 ФАЙЛЫ: • handlers/users.py: исправлен импорт Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
1181 lines
43 KiB
Python
1181 lines
43 KiB
Python
"""
|
||
Пользовательские команды
|
||
Доступны всем жильцам
|
||
"""
|
||
import logging
|
||
import json
|
||
from aiogram import Router, F
|
||
from aiogram.types import Message, CallbackQuery, ChatMemberUpdated, BotCommand
|
||
from aiogram.filters import Command, CommandStart, ChatMemberUpdatedFilter
|
||
from aiogram.enums import ChatMemberStatus
|
||
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 (ТОЛЬКО В ЛИЧКЕ!)"""
|
||
from utils.ui_styles import get_level_badge
|
||
|
||
# Проверяем что это личка
|
||
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()
|
||
|
||
logger.info(f'Новый пользователь {user_id} добавлен в базу')
|
||
|
||
# Приветственное сообщение
|
||
header = '🔴 <b>ДОБРО ПОЖАЛОВАТЬ</b>'
|
||
|
||
welcome_text = f'''{header}
|
||
|
||
👋 <b>Привет, {message.from_user.first_name}!</b>
|
||
|
||
{get_level_badge("unverified")}
|
||
|
||
Я — домовой бот нашего дома.
|
||
|
||
⚠️ <b>ВАЖНО:</b>
|
||
Для полного доступа пройдите верификацию!
|
||
|
||
📝 <b>Как пройти верификацию:</b>
|
||
|
||
<b>1️⃣ Отправьте номер квартиры</b>
|
||
Примеры: <code>45</code>, <code>12А</code>, <code>100-1</code>
|
||
|
||
<b>2️⃣ (опционально) Телефон</b>
|
||
Пример: <code>+79991234567</code>
|
||
|
||
<b>3️⃣ Дождитесь подтверждения</b>
|
||
Админ проверит и подтвердит.
|
||
|
||
После верификации откроются все функции! 🎉'''
|
||
|
||
await message.answer(welcome_text, parse_mode='HTML', reply_markup=get_unverified_menu())
|
||
return
|
||
|
||
# Пользователь в базе - обновляем имя
|
||
user.username = message.from_user.username
|
||
user.first_name = message.from_user.first_name
|
||
user.last_name = message.from_user.last_name
|
||
|
||
# Проверяем статус
|
||
is_admin = user.is_admin or (user_id == config.ADMIN_USER_ID)
|
||
is_superadmin = (user_id == config.ADMIN_USER_ID)
|
||
|
||
if user.verified:
|
||
# Проверяем членство в инициативной группе
|
||
from database.models import InitiativeGroup
|
||
ig_stmt = select(InitiativeGroup).where(
|
||
InitiativeGroup.user_id == user_id,
|
||
InitiativeGroup.is_active == True
|
||
)
|
||
ig_result = await session.execute(ig_stmt)
|
||
ig_member = ig_result.scalar_one_or_none()
|
||
is_ig = ig_member is not None
|
||
|
||
# Определяем уровень
|
||
if is_superadmin:
|
||
level = 'superadmin'
|
||
header = '🟣 <b>УПРАВЛЕНИЕ СИСТЕМОЙ</b>'
|
||
elif is_admin:
|
||
level = 'admin'
|
||
header = '🟡 <b>АДМИН-ПАНЕЛЬ</b>'
|
||
elif is_ig:
|
||
level = 'ig'
|
||
header = '🔵 <b>ИНИЦИАТИВНАЯ ГРУППА</b>'
|
||
else:
|
||
level = 'verified'
|
||
header = '🟢 <b>ДОСТУП РАЗРЕШЁН</b>'
|
||
|
||
await message.answer(
|
||
f'{header}\n\n'
|
||
f'👋 Здравствуйте, {message.from_user.first_name}!\n\n'
|
||
f'{get_level_badge(level)}\n\n'
|
||
f'✅ Вы верифицированы (кв. {user.apartment}).\n'
|
||
f'Выберите действие:',
|
||
reply_markup=get_main_menu(verified=True, is_ig=is_ig, is_admin=is_admin, is_superadmin=is_superadmin),
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
# Неверифицированный — подробная инструкция
|
||
verify_text = f'''🔴 <b>ТРЕБУЕТСЯ ВЕРИФИКАЦИЯ</b>
|
||
|
||
👋 Здравствуйте, {message.from_user.first_name}!
|
||
|
||
{get_level_badge("unverified")}
|
||
|
||
⏳ <b>Вы ещё не верифицированы</b>
|
||
|
||
📝 <b>Что делать?</b>
|
||
|
||
<b>1️⃣ Отправьте номер квартиры</b>
|
||
Примеры:
|
||
<code>45</code>
|
||
<code>12А</code>
|
||
<code>100-1</code>
|
||
|
||
<b>2️⃣ (опционально) Телефон</b>
|
||
Пример:
|
||
<code>+79991234567</code>
|
||
|
||
<b>3️⃣ Дождитесь подтверждения</b>
|
||
Админ проверит и подтвердит.
|
||
|
||
✅ После верификации получите доступ ко всем функциям!
|
||
|
||
💡 <b>Нужна помощь?</b>
|
||
Напишите администратору чата.'''
|
||
|
||
await message.answer(
|
||
verify_text,
|
||
reply_markup=get_unverified_menu(),
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
await session.commit()
|
||
|
||
|
||
@router.message(Command('помощь'))
|
||
@router.message(Command('help'))
|
||
async def cmd_help(message: Message):
|
||
"""Справка по командам (для жильцов)"""
|
||
from utils.ui_styles import get_colored_header, get_level_badge
|
||
from database.models import InitiativeGroup
|
||
|
||
user_id = message.from_user.id
|
||
async with AsyncSessionLocal() as session:
|
||
user = await session.get(User, user_id)
|
||
|
||
# Проверяем есть ли в базе
|
||
if not user:
|
||
logger.info(f'Пользователь {user_id} не в базе - игнорируем /help')
|
||
return
|
||
|
||
# Определяем уровень
|
||
if user.verified:
|
||
ig_stmt = select(InitiativeGroup).where(
|
||
InitiativeGroup.user_id == user_id,
|
||
InitiativeGroup.is_active == True
|
||
)
|
||
ig_result = await session.execute(ig_stmt)
|
||
is_ig = ig_result.scalar_one_or_none() is not None
|
||
is_admin = user.is_admin or (user_id == config.ADMIN_USER_ID)
|
||
|
||
if is_admin:
|
||
level = 'admin'
|
||
elif is_ig:
|
||
level = 'ig'
|
||
else:
|
||
level = 'verified'
|
||
else:
|
||
level = 'unverified'
|
||
|
||
# Базовые команды для ВСЕХ
|
||
text = f'''{get_colored_header('ПОМОЩЬ', level)}
|
||
|
||
{get_level_badge(level)}
|
||
|
||
<b>📱 Доступные команды:</b>
|
||
|
||
<b>Для всех:</b>
|
||
📞 /телефон — Экстренные службы
|
||
👷 /мастера — Сантехники, электрики
|
||
👮 /участковый — Контакты полиции
|
||
📜 /правила — Правила чата
|
||
❓ /помощь — Эта справка
|
||
|
||
<b>Верификация:</b>
|
||
Просто отправьте боту номер квартиры текстом
|
||
Примеры: <code>45</code>, <code>12А</code>, <code>100-1</code>
|
||
|
||
После верификации станут доступны:
|
||
• 📅 График отключений
|
||
• 💰 Платежи ЖКХ
|
||
• 🏆 Достижения
|
||
• 📋 Опросы
|
||
• 🔍 Моя квартира
|
||
'''
|
||
|
||
# Добавляем для админа
|
||
if level == 'admin':
|
||
text += '''
|
||
<b>⚙️ Администратору:</b>
|
||
/admin — Панель админа
|
||
/stats — Статистика чата
|
||
/spies — Подозрительные
|
||
/export — Экспорт JSON
|
||
/users — Список жильцов
|
||
/verify — Верифицировать
|
||
/broadcast — Рассылка
|
||
'''
|
||
|
||
# Добавляем для ИГ
|
||
if level == 'ig':
|
||
text += '''
|
||
<b>🎯 Инициативной группе:</b>
|
||
/ig_stats — Статистика ИГ
|
||
/ig_list — Список ИГ
|
||
'''
|
||
|
||
await message.answer(text, parse_mode='HTML', reply_markup=get_main_menu(
|
||
verified=(level != 'unverified'),
|
||
is_ig=(level == 'ig'),
|
||
is_admin=(level == 'admin'),
|
||
is_superadmin=False
|
||
))
|
||
|
||
|
||
@router.callback_query(F.data == 'help')
|
||
async def cb_help(callback: CallbackQuery):
|
||
"""Справка (callback)"""
|
||
await callback.answer()
|
||
text = '''<b>🏠 Домовой бот — помощь</b>
|
||
|
||
Этот бот помогает жильцам нашего дома быть в курсе событий и решать бытовые вопросы.
|
||
|
||
<b>🆕 Вы впервые здесь?</b>
|
||
|
||
1️⃣ <b>Пройдите верификацию</b> — это просто!
|
||
• Отправьте боту номер вашей квартиры
|
||
• Например: <code>45</code> или <code>12А</code>
|
||
• После проверки вы получите полный доступ
|
||
|
||
2️⃣ <b>Что будет после верификации?</b>
|
||
• 📅 График отключений воды и света
|
||
• 📢 Объявления и новости дома
|
||
• 💰 Напоминания о платежах ЖКХ
|
||
• 🏆 Рейтинг и достижения
|
||
• 📋 Участие в опросах
|
||
• 📅 События и собрания
|
||
|
||
<b>📞 Телефоны служб</b>
|
||
Нажмите "Экстренные телефоны" чтобы увидеть:
|
||
• 🚨 Аварийные службы (02, 03, 04, 112)
|
||
• 🔧 ЖКХ, сантехники, электрики
|
||
• 👮 Участковый полиция
|
||
|
||
<b>❓ Нужна помощь?</b>
|
||
Напишите администратору чата или используйте команду /help
|
||
|
||
<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 == 'rules')
|
||
async def cb_rules(callback: CallbackQuery):
|
||
"""Правила чата"""
|
||
await callback.answer()
|
||
text = '''<b>📜 Правила нашего чата</b>
|
||
|
||
1️⃣ <b>Уважайте соседей</b>
|
||
• Не оскорбляйте и не унижайте других
|
||
• Запрещены угрозы и дискриминация
|
||
• Критикуйте идеи, а не людей
|
||
|
||
2️⃣ <b>Не спамьте</b>
|
||
• Максимум 10 сообщений в минуту
|
||
• Запрещена реклама без согласования
|
||
• Не отправляйте цепочки и фейки
|
||
|
||
3️⃣ <b>Мат — под запретом</b>
|
||
• 3 предупреждения = бан
|
||
• Используйте цензурную лексику
|
||
|
||
4️⃣ <b>Темы для обсуждений</b>
|
||
• Вопросы по дому и ЖКХ
|
||
• Соседи и взаимопомощь
|
||
• Объявления (продам, куплю, отдам)
|
||
• События и собрания
|
||
|
||
5️⃣ <b>Верификация обязательна</b>
|
||
• Отправьте номер квартиры боту
|
||
• Без верификации доступ ограничен
|
||
• Это защита от шпионов и ботов
|
||
|
||
⚠️ <b>Нарушения</b>
|
||
• Предупреждение (устно)
|
||
• Мут на 30 минут
|
||
• Бан навсегда
|
||
|
||
💡 <b>Хотите помочь чату?</b>
|
||
Вступайте в инициативную группу!'''
|
||
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@router.message(Command('правила'))
|
||
@router.message(Command('rules'))
|
||
async def cmd_rules(message: Message):
|
||
"""Правила чата (команда)"""
|
||
text = '''<b>📜 Правила нашего чата</b>
|
||
|
||
1️⃣ <b>Уважайте соседей</b>
|
||
• Не оскорбляйте и не унижайте других
|
||
• Запрещены угрозы и дискриминация
|
||
• Критикуйте идеи, а не людей
|
||
|
||
2️⃣ <b>Не спамьте</b>
|
||
• Максимум 10 сообщений в минуту
|
||
• Запрещена реклама без согласования
|
||
• Не отправляйте цепочки и фейки
|
||
|
||
3️⃣ <b>Мат — под запретом</b>
|
||
• 3 предупреждения = бан
|
||
• Используйте цензурную лексику
|
||
|
||
4️⃣ <b>Темы для обсуждений</b>
|
||
• Вопросы по дому и ЖКХ
|
||
• Соседи и взаимопомощь
|
||
• Объявления (продам, куплю, отдам)
|
||
• События и собрания
|
||
|
||
5️⃣ <b>Верификация обязательна</b>
|
||
• Отправьте номер квартиры боту
|
||
• Без верификации доступ ограничен
|
||
• Это защита от шпионов и ботов
|
||
|
||
⚠️ <b>Нарушения</b>
|
||
• Предупреждение (устно)
|
||
• Мут на 30 минут
|
||
• Бан навсегда
|
||
|
||
💡 <b>Хотите помочь чату?</b>
|
||
Вступайте в инициативную группу!'''
|
||
|
||
await message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
@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):
|
||
"""Главное меню"""
|
||
from utils.ui_styles import get_colored_header, get_level_badge
|
||
|
||
await callback.answer()
|
||
|
||
user_id = callback.from_user.id
|
||
async with AsyncSessionLocal() as session:
|
||
user = await session.get(User, user_id)
|
||
|
||
if user and user.verified:
|
||
# Проверяем ИГ и админа
|
||
from database.models import InitiativeGroup
|
||
ig_stmt = select(InitiativeGroup).where(
|
||
InitiativeGroup.user_id == user_id,
|
||
InitiativeGroup.is_active == True
|
||
)
|
||
ig_result = await session.execute(ig_stmt)
|
||
is_ig = ig_result.scalar_one_or_none() is not None
|
||
is_admin = user.is_admin or (user_id == config.ADMIN_USER_ID)
|
||
is_superadmin = (user_id == config.ADMIN_USER_ID)
|
||
|
||
# Определяем уровень
|
||
if is_superadmin:
|
||
level = 'superadmin'
|
||
header = get_colored_header('УПРАВЛЕНИЕ СИСТЕМОЙ', level)
|
||
elif is_admin:
|
||
level = 'admin'
|
||
header = get_colored_header('АДМИН-ПАНЕЛЬ', level)
|
||
elif is_ig:
|
||
level = 'ig'
|
||
header = get_colored_header('ИНИЦИАТИВНАЯ ГРУППА', level)
|
||
else:
|
||
level = 'verified'
|
||
header = get_colored_header('ГЛАВНОЕ МЕНЮ', level)
|
||
|
||
await callback.message.answer(
|
||
f'{header}\n\n{get_level_badge(level)}',
|
||
reply_markup=get_main_menu(verified=True, is_ig=is_ig, is_admin=is_admin, is_superadmin=is_superadmin),
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
header = get_colored_header('МЕНЮ', 'unverified')
|
||
await callback.message.answer(
|
||
f'{header}\n\n{get_level_badge("unverified")}',
|
||
reply_markup=get_unverified_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(verified=True)
|
||
)
|
||
# Перенаправляем на 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] = {}
|
||
|
||
|
||
# Проверка что сообщение в личке
|
||
def check_private_chat(callback: CallbackQuery) -> bool:
|
||
"""Проверяет что callback из лички"""
|
||
if callback.message.chat.type != 'private':
|
||
callback.answer('💡 Это работает только в личных сообщениях!', show_alert=True)
|
||
return False
|
||
return True
|
||
|
||
|
||
@router.callback_query(F.data == 'achievements')
|
||
async def cb_achievements_menu(callback: CallbackQuery):
|
||
"""Кнопка Достижения"""
|
||
if not check_private_chat(callback):
|
||
return
|
||
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):
|
||
"""Кнопка События"""
|
||
if not check_private_chat(callback):
|
||
return
|
||
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):
|
||
"""Кнопка Платежи"""
|
||
if not check_private_chat(callback):
|
||
return
|
||
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):
|
||
"""Кнопка Опросы"""
|
||
if not check_private_chat(callback):
|
||
return
|
||
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):
|
||
"""Кнопка Поблагодарить - выбор пользователя"""
|
||
if not check_private_chat(callback):
|
||
return
|
||
await callback.answer()
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
# Показываем ВСЕХ верифицированных пользователей
|
||
stmt = select(User).where(User.verified == True).order_by(desc(User.message_count))
|
||
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
|
||
}
|
||
|
||
total_pages = (len(users) + 19) // 20
|
||
|
||
await callback.message.answer(
|
||
f'🙏 <b>Поблагодарить пользователя</b>\n\n'
|
||
f'Всего верифицированных: {len(users)}\n'
|
||
f'Страница 1 из {total_pages}\n\n'
|
||
f'Выберите пользователя:\n'
|
||
f'(+10 к рейтингу)\n\n'
|
||
f'💡 <b>Совет:</b> Используйте /thank @username для быстрого поиска!',
|
||
reply_markup=get_thank_user_keyboard(users, page=0, page_size=20),
|
||
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 'не указана'
|
||
phone = user.phone or 'не указан'
|
||
|
||
text = '🔍 <b>Ваша квартира</b>\n\n'
|
||
text += f'🏠 Квартира: <code>{apt}</code>\n'
|
||
text += f'📞 Телефон: <code>{phone}</code>\n\n'
|
||
text += '<b>Хотите изменить?</b>\n\n'
|
||
text += 'Используйте команды:\n'
|
||
text += '<code>/setapt 45</code> — изменить квартиру\n'
|
||
text += '<code>/setphone +79991234567</code> — изменить телефон'
|
||
|
||
await callback.message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
# =============================================================================
|
||
# ИЗМЕНИТЬ ПРОФИЛЬ (КОМАНДЫ)
|
||
# =============================================================================
|
||
|
||
@router.message(Command('setapt'))
|
||
@router.message(Command('set_apt'))
|
||
async def cmd_set_apt(message: Message):
|
||
"""
|
||
Изменить номер квартиры
|
||
Использование: /setapt 45
|
||
"""
|
||
from utils.validators import validate_apartment
|
||
|
||
user_id = message.from_user.id
|
||
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/setapt 45</code>\n\n'
|
||
'Где 45 — ваш номер квартиры.',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
apartment = args[1].strip().upper()
|
||
|
||
if not validate_apartment(apartment):
|
||
await message.answer(
|
||
'❌ Неверный формат квартиры.\n\n'
|
||
'Примеры: <code>45</code>, <code>12А</code>, <code>100-1</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
user = await session.get(User, user_id)
|
||
|
||
if not user:
|
||
await message.answer('❌ Вы не зарегистрированы')
|
||
return
|
||
|
||
user.apartment = apartment
|
||
await session.commit()
|
||
|
||
await message.answer(
|
||
f'✅ <b>Квартира обновлена!</b>\n\n'
|
||
f'Ваша квартира: <code>{apartment}</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
@router.message(Command('setphone'))
|
||
@router.message(Command('set_phone'))
|
||
async def cmd_set_phone(message: Message):
|
||
"""
|
||
Изменить номер телефона
|
||
Использование: /setphone +79991234567
|
||
"""
|
||
user_id = message.from_user.id
|
||
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/setphone +79991234567</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
phone = args[1].strip()
|
||
|
||
# Простая валидация
|
||
if not phone.startswith('+') or len(phone) < 10:
|
||
await message.answer(
|
||
'❌ Неверный формат телефона.\n\n'
|
||
'Пример: <code>+79991234567</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
user = await session.get(User, user_id)
|
||
|
||
if not user:
|
||
await message.answer('❌ Вы не зарегистрированы')
|
||
return
|
||
|
||
user.phone = phone
|
||
await session.commit()
|
||
|
||
await message.answer(
|
||
f'✅ <b>Телефон обновлён!</b>\n\n'
|
||
f'Ваш телефон: <code>{phone}</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# РЕДАКТИРОВАНИЕ ПРОФИЛЯ ЖИЛЬЦА (ДЛЯ АДМИНА)
|
||
# =============================================================================
|
||
|
||
@router.message(Command('edit_user'))
|
||
@router.message(Command('edituser'))
|
||
async def cmd_edit_user(message: Message):
|
||
"""
|
||
Редактировать профиль жильца (для админа)
|
||
Использование: /edit_user <user_id> [квартира] [телефон]
|
||
"""
|
||
if not config.is_admin(message.from_user.id):
|
||
await message.answer('❌ Только для администраторов')
|
||
return
|
||
|
||
args = message.text.split()
|
||
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n\n'
|
||
'<code>/edit_user <user_id> [квартира] [телефон]</code>\n\n'
|
||
'Примеры:\n'
|
||
'<code>/edit_user 123456789 45 +79991234567</code>\n'
|
||
'<code>/edit_user 123456789 apt:45 phone:+79991234567</code>\n'
|
||
'<code>/edit_user 123456789 phone:+79991234567</code>\n'
|
||
'<code>/edit_user 123456789 apt:45</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
try:
|
||
user_id = int(args[1])
|
||
except ValueError:
|
||
await message.answer('❌ Неверный ID пользователя')
|
||
return
|
||
|
||
# Парсим аргументы
|
||
apartment = None
|
||
phone = None
|
||
|
||
for arg in args[2:]:
|
||
if arg.startswith('apt:'):
|
||
apartment = arg[4:].upper()
|
||
elif arg.startswith('phone:'):
|
||
phone = arg[6:]
|
||
elif apartment is None:
|
||
# Первый аргумент без префикса — квартира
|
||
apartment = arg.upper()
|
||
elif phone is None:
|
||
# Второй аргумент без префикса — телефон
|
||
phone = arg
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
user = await session.get(User, user_id)
|
||
|
||
if not user:
|
||
await message.answer(f'❌ Пользователь {user_id} не найден')
|
||
return
|
||
|
||
# Обновляем данные
|
||
updated = []
|
||
|
||
if apartment:
|
||
user.apartment = apartment
|
||
updated.append(f'квартира: {apartment}')
|
||
|
||
if phone:
|
||
user.phone = phone
|
||
updated.append(f'телефон: {phone}')
|
||
|
||
if not updated:
|
||
await message.answer(
|
||
'❌ Укажите квартиру или телефон\n\n'
|
||
'Пример: <code>/edit_user 123456789 45 +79991234567</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
await session.commit()
|
||
|
||
name = user.full_name or user.username or f'user{user_id}'
|
||
|
||
await message.answer(
|
||
f'✅ <b>Профиль обновлён!</b>\n\n'
|
||
f'👤 {name}\n'
|
||
f'🔖 ID: <code>{user_id}</code>\n\n'
|
||
f'Обновлено:\n' +
|
||
'\n'.join(f'• {item}' for item in updated),
|
||
parse_mode='HTML'
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# ПРИВЕТСТВИЕ НОВЫХ УЧАСТНИКОВ
|
||
# =============================================================================
|
||
|
||
@router.chat_member(ChatMemberUpdatedFilter(ChatMemberStatus.LEFT, ChatMemberStatus.MEMBER))
|
||
async def welcome_new_member(event: ChatMemberUpdated, bot: Bot):
|
||
"""Приветствие нового участника группы"""
|
||
from aiogram.exceptions import TelegramForbiddenError
|
||
|
||
user = event.from_user
|
||
chat = event.chat
|
||
|
||
logger.info(f'Новый участник в чате {chat.title}: {user.first_name} (@{user.username})')
|
||
|
||
# Отправляем привет в личку
|
||
try:
|
||
bot_username = (await bot.get_me()).username
|
||
|
||
welcome_text = f'''🏠 <b>Добро пожаловать в чат ЖК "Ленком 37"!</b>
|
||
|
||
👋 Привет, {user.first_name}!
|
||
|
||
Я — домовой бот нашего дома.
|
||
|
||
📋 <b>Что здесь происходит:</b>
|
||
• Общение соседей
|
||
• Объявления и новости
|
||
• Вопросы по ЖКХ
|
||
• Взаимопомощь
|
||
|
||
✅ <b>Чтобы получить полный доступ:</b>
|
||
1. Напиши мне в личку: @{bot_username}
|
||
2. Отправь номер квартиры (например: <code>45</code>)
|
||
3. Дождись подтверждения админа
|
||
|
||
📞 <b>Полезные команды:</b>
|
||
/телефон — Экстренные службы
|
||
/мастера — Сантехники, электрики
|
||
/участковый — Контакты полиции
|
||
/график — Отключения воды/света
|
||
/правила — Правила чата
|
||
|
||
💡 <b>Нужна помощь?</b>
|
||
Напишите администратору чата.
|
||
|
||
🎉 <b>Рады видеть вас в нашем сообществе!</b>'''
|
||
|
||
await bot.send_message(
|
||
chat_id=user.id,
|
||
text=welcome_text,
|
||
parse_mode='HTML'
|
||
)
|
||
logger.info(f'Приветствие отправлено {user.id}')
|
||
|
||
except TelegramForbiddenError:
|
||
logger.warning(f'Не удалось отправить приветствие {user.id} (закрыт от ботов)')
|
||
except Exception as e:
|
||
logger.error(f'Ошибка при отправке приветствия: {e}')
|