466 lines
No EOL
17 KiB
Python
466 lines
No EOL
17 KiB
Python
"""
|
||
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(
|
||
'📅 <b>Создание события</b>\n\n'
|
||
'Отправьте название события:\n'
|
||
'<i>Например: "Общее собрание жильцов"</i>',
|
||
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'
|
||
'<code>ДД.ММ.ГГГГ ЧЧ:ММ</code>\n\n'
|
||
'<i>Например: 01.03.2026 19:00</i>',
|
||
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'
|
||
'Используйте: <code>ДД.ММ.ГГГГ ЧЧ:ММ</code>',
|
||
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'
|
||
'<i>Например: "Холл 1 этажа"</i>',
|
||
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'✅ <b>Событие создано!</b>\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 = '📅 <b>Предстоящие события</b>\n\n'
|
||
if not events:
|
||
text += 'Событий пока нет. Следите за обновлениями!'
|
||
else:
|
||
for e in events:
|
||
text += f'• <b>{e["title"]}</b> ({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} <b>{event["title"]}</b>\n\n'
|
||
f'🗓️ <b>Дата:</b> {event["event_date"].strftime("%d.%m.%Y %H:%M")}\n'
|
||
f'📍 <b>Место:</b> {event["location"] or "Не указано"}\n'
|
||
f'👥 <b>Участников:</b> {event["participants_count"]}\n\n'
|
||
f'📝 <b>Описание:</b>\n{event["description"] or "Нет описания"}'
|
||
)
|
||
|
||
if participants:
|
||
text += '\n\n<b>Участники:</b>\n'
|
||
for p in participants[:10]:
|
||
text += f'• {p["name"]}\n'
|
||
if len(participants) > 10:
|
||
text += f'<i>...и еще {len(participants)-10}</i>'
|
||
|
||
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 <event_id>
|
||
"""
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/join <event_id></code>',
|
||
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'✅ <b>Вы записались на событие!</b>\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 <event_id>
|
||
"""
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/leave <event_id></code>',
|
||
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('📅 <b>Вы не записаны на события</b>\n\n'
|
||
'Посмотреть все: /events',
|
||
parse_mode='HTML')
|
||
return
|
||
|
||
text = '📅 <b>Ваши события</b>\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 <event_id>
|
||
"""
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/participants <event_id></code>',
|
||
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'👥 <b>Участники: {event["title"]}</b>\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 <event_id>
|
||
"""
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/event_cancel <event_id></code>',
|
||
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('❌ Событие не найдено') |