domovoy_bot/handlers/users.py
Admin cef2ac3714 v1.19 ИГ рассылка + объявления 14 дней + настройки
🔥 ОБЪЯВЛЕНИЯ:
• Срок действия: 7 → 14 дней

🎯 ИНИЦИАТИВНАЯ ГРУППА:
• Добавлена кнопка 'Рассылка ИГ' в меню
• Команда /ig_broadcast для членов ИГ
• Поддержка текста + фото + файлов
• Настройка лимита в веб-панели

⚙️ ВЕБ-ПАНЕЛЬ:
• Новая страница /settings
• Настройка лимита рассылки ИГ (секунды)
• Кнопки статистики фильтруют пользователей
• Индикатор активного фильтра

🎨 UI:
• Исправлен <font> → emoji (Telegram не поддерживает)
• Цветные заголовки через emoji

📁 ФАЙЛЫ:
• handlers/ads.py: 14 дней
• handlers/initiative_group.py: рассылка ИГ
• web/app.py: API настроек
• web/templates/settings.html: страница настроек
• utils/ui_styles.py: без <font> тегов
• database: settings table

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-10 17:01:30 +00:00

1126 lines
41 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 (ТОЛЬКО В ЛИЧКЕ!)"""
from utils.ui_styles import get_level_badge
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton
# Проверяем что это личка
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()
# 🔴 НОВОЕ СООБЩЕНИЕ С КНОПКАМИ (БЕЗ <font>!)
welcome_text = f'''🔴 <b>ДОБРО ПОЖАЛОВАТЬ</b>
👋 <b>Привет, {message.from_user.first_name}!</b>
Я — домовой бот нашего дома.
🏠 <b>Что я умею:</b>
• 📞 Телефоны экстренных служб
• 👷 Проверенные мастера
• 📅 График отключений
• 💰 Напоминания о платежах
• 🏆 Рейтинг и достижения
• 📋 Опросы и голосования
⚠️ <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> Дождитесь подтверждения админа
После верификации откроются все функции! 🎉'''
# Кнопки для быстрого старта
kb = ReplyKeyboardMarkup(
keyboard=[
[KeyboardButton(text='📞 Экстренные телефоны')],
[KeyboardButton(text='📜 Правила чата'), KeyboardButton(text='❓ Помощь')],
],
resize_keyboard=True
)
await message.answer(welcome_text, parse_mode='HTML', reply_markup=kb)
else:
# Обновляем имя
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:
# Неверифицированный — подробная инструкция (БЕЗ <font>!)
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, get_user_level_with_ig
from database.models import InitiativeGroup
user_id = message.from_user.id
async with AsyncSessionLocal() as session:
user = await session.get(User, user_id)
# Определяем уровень
if user and 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).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 'не указана'
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 &lt;user_id&gt; [квартира] [телефон]</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'
)