"""
Events — календарь событий дома
"""
import logging
from datetime import datetime, timedelta
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery
from aiogram.filters import Command, StateFilter
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from database.db import AsyncSessionLocal
from database.models import User
from services.events import EventsService
from utils.decorators import admin_only
from keyboards.inline import get_events_keyboard, get_event_view_keyboard, get_main_menu
from swarmlib.notifier import send_telegram
from sqlalchemy import select
logger = logging.getLogger(__name__)
router = Router()
# =============================================================================
# FSM для создания события
# =============================================================================
class EventState(StatesGroup):
waiting_for_title = State()
waiting_for_date = State()
waiting_for_location = State()
waiting_for_description = State()
# =============================================================================
# СОЗДАНИЕ СОБЫТИЯ
# =============================================================================
@router.message(Command('event_add'))
@router.message(Command('добавить_событие'))
@admin_only
async def cmd_event_add(message: Message, state: FSMContext):
"""
Добавить событие
Использование: /event_add
"""
await message.answer(
'📅 Создание события\n\n'
'Отправьте название события:\n'
'Например: "Общее собрание жильцов"',
parse_mode='HTML'
)
await state.set_state(EventState.waiting_for_title)
@router.message(EventState.waiting_for_title)
async def process_event_title(message: Message, state: FSMContext):
"""Получили название события"""
await state.update_data(title=message.text)
await message.answer(
f'✅ Название: {message.text}\n\n'
'Отправьте дату и время события в формате:\n'
'ДД.ММ.ГГГГ ЧЧ:ММ\n\n'
'Например: 01.03.2026 19:00',
parse_mode='HTML'
)
await state.set_state(EventState.waiting_for_date)
@router.message(EventState.waiting_for_date)
async def process_event_date(message: Message, state: FSMContext):
"""Получили дату события"""
text = message.text.strip()
try:
event_date = datetime.strptime(text, '%d.%m.%Y %H:%M')
except ValueError:
await message.answer(
'❌ Неверный формат даты.\n'
'Используйте: ДД.ММ.ГГГГ ЧЧ:ММ',
parse_mode='HTML'
)
return
if event_date < datetime.utcnow():
await message.answer('❌ Дата должна быть в будущем!')
return
await state.update_data(event_date=event_date)
await message.answer(
f'✅ Дата: {event_date.strftime("%d.%m.%Y %H:%M")}\n\n'
'Отправьте место проведения (необязательно) или "пропустить":\n'
'Например: "Холл 1 этажа"',
parse_mode='HTML'
)
await state.set_state(EventState.waiting_for_location)
@router.message(EventState.waiting_for_location)
async def process_event_location(message: Message, state: FSMContext):
"""Получили место проведения"""
location = message.text if message.text.lower() != 'пропустить' else None
await state.update_data(location=location)
await message.answer(
f'✅ Место: {location or "Не указано"}\n\n'
'Отправьте описание события (необязательно) или "пропустить":',
parse_mode='HTML'
)
await state.set_state(EventState.waiting_for_description)
@router.message(EventState.waiting_for_description)
async def process_event_description(message: Message, state: FSMContext):
"""Получили описание"""
description = message.text if message.text.lower() != 'пропустить' else None
await state.update_data(description=description)
# Создаём событие
data = await state.get_data()
async with AsyncSessionLocal() as session:
events_service = EventsService(session)
event_id = await events_service.create_event(
title=data['title'],
event_date=data['event_date'],
description=description,
location=data.get('location'),
created_by=message.from_user.id
)
# Уведомляем админа через swarmlib
send_telegram(f"📅 *Новое событие создано!*\n\n"
f"Название: {data['title']}\n"
f"Дата: {data['event_date'].strftime('%d.%m.%Y %H:%M')}\n"
f"Автор: {message.from_user.full_name} (ID: {message.from_user.id})",
parse_mode="markdown")
await message.answer(
f'✅ Событие создано!\n\n'
f'📅 {data["title"]}\n'
f'🗓️ {data["event_date"].strftime("%d.%m.%Y %H:%M")}\n'
f'📍 {data.get("location") or "Место не указано"}\n'
f'🔖 ID: {event_id}\n\n'
f'Событие доступно в меню "События"',
parse_mode='HTML'
)
await state.clear()
# =============================================================================
# ПРОСМОТР СОБЫТИЙ (INLINE)
# =============================================================================
@router.message(Command('events'))
@router.message(Command('события'))
@router.callback_query(F.data == 'events')
async def cmd_events(event):
"""Показать предстоящие события (Inline)"""
async with AsyncSessionLocal() as session:
events_service = EventsService(session)
events = await events_service.get_upcoming_events(10)
text = '📅 Предстоящие события\n\n'
if not events:
text += 'Событий пока нет. Следите за обновлениями!'
else:
for e in events:
text += f'• {e["title"]} ({e["event_date"].strftime("%d.%m.%Y %H:%M")})\n'
keyboard = get_events_keyboard(events)
if isinstance(event, Message):
await event.answer(text, reply_markup=keyboard, parse_mode='HTML')
else:
await event.message.edit_text(text, reply_markup=keyboard, parse_mode='HTML')
@router.callback_query(F.data.startswith('event_view_'))
async def cb_event_view(callback: CallbackQuery):
"""Детальный просмотр события"""
event_id = int(callback.data.split('_')[2])
user_id = callback.from_user.id
async with AsyncSessionLocal() as session:
events_service = EventsService(session)
event = await events_service.get_event(event_id)
if not event:
await callback.answer('❌ Событие не найдено', show_alert=True)
return
participants = await events_service.get_participants(event_id)
is_participant = any(p['user_id'] == user_id for p in participants)
# Проверяем админа для кнопки удаления
stmt = select(User).where(User.user_id == user_id)
user = (await session.execute(stmt)).scalar_one_or_none()
is_admin = user.is_admin if user else False
emoji = events_service.get_event_type_emoji(event['event_type'])
text = (
f'{emoji} {event["title"]}\n\n'
f'🗓️ Дата: {event["event_date"].strftime("%d.%m.%Y %H:%M")}\n'
f'📍 Место: {event["location"] or "Не указано"}\n'
f'👥 Участников: {event["participants_count"]}\n\n'
f'📝 Описание:\n{event["description"] or "Нет описания"}'
)
if participants:
text += '\n\nУчастники:\n'
for p in participants[:10]:
text += f'• {p["name"]}\n'
if len(participants) > 10:
text += f'...и еще {len(participants)-10}'
await callback.message.edit_text(
text,
reply_markup=get_event_view_keyboard(event_id, is_participant, is_admin),
parse_mode='HTML'
)
@router.callback_query(F.data.startswith('event_join_'))
async def cb_event_join(callback: CallbackQuery):
"""Записаться на событие через кнопку"""
event_id = int(callback.data.split('_')[2])
user_id = callback.from_user.id
async with AsyncSessionLocal() as session:
events_service = EventsService(session)
success = await events_service.join_event(event_id, user_id)
if success:
await callback.answer('✅ Вы записались!', show_alert=False)
# Обновляем сообщение
await cb_event_view(callback)
else:
await callback.answer('ℹ️ Вы уже участвуете', show_alert=True)
@router.callback_query(F.data.startswith('event_leave_'))
async def cb_event_leave(callback: CallbackQuery):
"""Отписаться от события через кнопку"""
event_id = int(callback.data.split('_')[2])
user_id = callback.from_user.id
async with AsyncSessionLocal() as session:
events_service = EventsService(session)
await events_service.leave_event(event_id, user_id)
await callback.answer('✅ Вы отписались', show_alert=False)
await cb_event_view(callback)
@router.callback_query(F.data.startswith('event_delete_'))
@admin_only
async def cb_event_delete(callback: CallbackQuery):
"""Удалить событие (Админ)"""
event_id = int(callback.data.split('_')[2])
async with AsyncSessionLocal() as session:
events_service = EventsService(session)
event = await events_service.get_event(event_id)
await events_service.cancel_event(event_id)
send_telegram(f"🗑️ *Событие удалено!*\n\nID: {event_id}\nНазвание: {event['title'] if event else '???'}\nАдмин: {callback.from_user.full_name}", parse_mode="markdown")
await callback.answer('🗑️ Событие удалено', show_alert=True)
await cmd_events(callback)
# =============================================================================
# УЧАСТИЕ В СОБЫТИИ (СТАРЫЕ КОМАНДЫ ДЛЯ СОВМЕСТИМОСТИ)
# =============================================================================
@router.message(Command('join'))
async def cmd_join(message: Message):
"""
Присоединиться к событию
Использование: /join
"""
args = message.text.split()
if len(args) < 2:
await message.answer(
'❌ Использование:\n'
'/join <event_id>',
parse_mode='HTML'
)
return
try:
event_id = int(args[1])
except ValueError:
await message.answer('❌ Неверный ID события')
return
async with AsyncSessionLocal() as session:
events_service = EventsService(session)
event = await events_service.get_event(event_id)
if not event:
await message.answer('❌ Событие не найдено')
return
if not event['is_active']:
await message.answer('❌ Событие отменено')
return
success = await events_service.join_event(event_id, message.from_user.id)
if success:
await message.answer(
f'✅ Вы записались на событие!\n\n'
f'📅 {event["title"]}\n'
f'🗓️ {event["event_date"].strftime("%d.%m.%Y %H:%M")}\n'
f'👥 Участников: {event["participants_count"] + 1}',
parse_mode='HTML'
)
else:
await message.answer('ℹ️ Вы уже записаны на это событие')
@router.message(Command('leave'))
async def cmd_leave(message: Message):
"""
Отписаться от события
Использование: /leave
"""
args = message.text.split()
if len(args) < 2:
await message.answer(
'❌ Использование:\n'
'/leave <event_id>',
parse_mode='HTML'
)
return
try:
event_id = int(args[1])
except ValueError:
await message.answer('❌ Неверный ID события')
return
async with AsyncSessionLocal() as session:
events_service = EventsService(session)
success = await events_service.leave_event(event_id, message.from_user.id)
if success:
await message.answer('✅ Вы отписались от события')
else:
await message.answer('ℹ️ Вы не записаны на это событие')
# =============================================================================
# МОИ СОБЫТИЯ
# =============================================================================
@router.message(Command('my_events'))
@router.message(Command('мои_события'))
async def cmd_my_events(message: Message):
"""Показать события пользователя"""
async with AsyncSessionLocal() as session:
events_service = EventsService(session)
events = await events_service.get_user_events(message.from_user.id)
if not events:
await message.answer('📅 Вы не записаны на события\n\n'
'Посмотреть все: /events',
parse_mode='HTML')
return
text = '📅 Ваши события\n\n'
for event in events:
date_str = event['event_date'].strftime('%d.%m.%Y %H:%M')
text += f'📅 {event["title"]}\n'
text += f' └─ {date_str}\n'
text += f' └─ 📍 {event["location"] or "Место не указано"}\n\n'
await message.answer(text, parse_mode='HTML')
# =============================================================================
# УЧАСТНИКИ СОБЫТИЯ
# =============================================================================
@router.message(Command('participants'))
@router.message(Command('участники'))
@admin_only
async def cmd_participants(message: Message):
"""
Показать участников события
Использование: /participants
"""
args = message.text.split()
if len(args) < 2:
await message.answer(
'❌ Использование:\n'
'/participants <event_id>',
parse_mode='HTML'
)
return
try:
event_id = int(args[1])
except ValueError:
await message.answer('❌ Неверный ID события')
return
async with AsyncSessionLocal() as session:
events_service = EventsService(session)
participants = await events_service.get_participants(event_id)
event = await events_service.get_event(event_id)
if not participants:
await message.answer('ℹ️ Нет участников')
return
text = f'👥 Участники: {event["title"]}\n\n'
text += f'Всего: {len(participants)}\n\n'
for p in participants:
apt = f'(кв. {p["apartment"]})' if p['apartment'] else ''
text += f'• {p["name"]} {apt}\n'
await message.answer(text, parse_mode='HTML')
# =============================================================================
# ОТМЕНА СОБЫТИЯ
# =============================================================================
@router.message(Command('event_cancel'))
@router.message(Command('отменить_событие'))
@admin_only
async def cmd_event_cancel(message: Message):
"""
Отменить событие
Использование: /event_cancel
"""
args = message.text.split()
if len(args) < 2:
await message.answer(
'❌ Использование:\n'
'/event_cancel <event_id>',
parse_mode='HTML'
)
return
try:
event_id = int(args[1])
except ValueError:
await message.answer('❌ Неверный ID события')
return
async with AsyncSessionLocal() as session:
events_service = EventsService(session)
success = await events_service.cancel_event(event_id)
if success:
await message.answer('✅ Событие отменено')
else:
await message.answer('❌ Событие не найдено')