domovoy_bot/handlers/users.py
Admin 5adb3d7818 🔒 ЗАЩИТА ОТ СПАМА В ОБЩИЙ ЧАТ
 Все команды теперь работают ТОЛЬКО В ЛИЧКЕ:
- /start, /help - только в личке
- Все кнопки меню - только в личке
- Админ-команды - только в личке (уже было)

📝 Что будет если написать в общем чате:
'💡 Бот работает только в личных сообщениях!
Нажмите на имя бота сверху и напишите /start в личке'

🎯 Преимущества:
- Нет спама в общем чате
- Все диалоги конфиденциальны
- Жильцы общаются с ботом в личке
- Админы управляют через личку

💡 Исключения:
- Мониторинг сообщений (анти-мат, анти-спам)
- Детект шпионов
- Рейтинги активности
- Эти функции работают в чате но не спам!

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-03 18:25:21 +00:00

640 lines
23 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Пользовательские команды
Доступны всем жильцам
"""
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 (ТОЛЬКО В ЛИЧКЕ!)"""
# Проверяем что это личка
if message.chat.type != 'private':
await message.answer(
'💡 <b>Бот работает только в личных сообщениях!</b>\n\n'
'Нажмите на имя бота сверху и напишите /start в личке',
parse_mode='HTML'
)
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'🏠 <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] = {}
# Проверка что сообщение в личке
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).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')