✅ Регистрация и верификация жильцов ✅ Рейтинг активности (уровни) ✅ Детект шпионов (Score 0-100) ✅ Анти-мат система (3 предупреждения → бан) ✅ Админ-панель с рассылкой ✅ Учёт квартир (несколько жильцов) ✅ Телефоны экстренных служб и мастеров ✅ Еженедельный экспорт JSON ✅ Прокси (socks5) для обхода РКН Бекап: backups/versions/v1.0_mvp_2026-02-26/ Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
368 lines
No EOL
13 KiB
Python
368 lines
No EOL
13 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 services.events import EventsService
|
||
from utils.decorators import admin_only
|
||
|
||
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
|
||
)
|
||
|
||
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'Для просмотра: /events',
|
||
parse_mode='HTML'
|
||
)
|
||
await state.clear()
|
||
|
||
|
||
# =============================================================================
|
||
# ПРОСМОТР СОБЫТИЙ
|
||
# =============================================================================
|
||
|
||
@router.message(Command('events'))
|
||
@router.message(Command('события'))
|
||
async def cmd_events(message: Message):
|
||
"""Показать предстоящие события"""
|
||
async with AsyncSessionLocal() as session:
|
||
events_service = EventsService(session)
|
||
events = await events_service.get_upcoming_events(10)
|
||
|
||
if not events:
|
||
await message.answer('📅 <b>Предстоящих событий нет</b>\n\n'
|
||
'Добавить событие: /event_add',
|
||
parse_mode='HTML')
|
||
return
|
||
|
||
text = '📅 <b>Предстоящие события</b>\n\n'
|
||
|
||
for event in events:
|
||
emoji = events_service.get_event_type_emoji(event['event_type'])
|
||
date_str = event['event_date'].strftime('%d.%m.%Y %H:%M')
|
||
|
||
text += f'{emoji} <b>{event["title"]}</b>\n'
|
||
text += f' └─ {date_str}\n'
|
||
text += f' └─ 📍 {event["location"] or "Место не указано"}\n'
|
||
text += f' └─ 🔖 ID: {event["id"]}\n\n'
|
||
|
||
text += '💡 Для участия: /join <ID>'
|
||
|
||
await message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
# =============================================================================
|
||
# УЧАСТИЕ В СОБЫТИИ
|
||
# =============================================================================
|
||
|
||
@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('❌ Событие не найдено') |