""" Initiative Group — хендлеры для работы с инициативной группой Только для администраторов """ import logging 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.initiative_group import InitiativeGroupService from utils.decorators import admin_only import config logger = logging.getLogger(__name__) router = Router() # ============================================================================= # МЕНЮ ИНИЦИАТИВНОЙ ГРУППЫ (CALLBACK) # ============================================================================= @router.callback_query(F.data == 'ig_menu') async def cb_ig_menu(callback: CallbackQuery): """Меню инициативной группы""" from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from utils.ui_styles import get_colored_header, get_level_badge from config import ADMIN_USER_ID await callback.answer() user_id = callback.from_user.id async with AsyncSessionLocal() as session: ig_service = InitiativeGroupService(session) is_member = await ig_service.is_member(user_id) # Админы и суперадмин всегда имеют доступ is_admin = user_id == ADMIN_USER_ID if is_member or is_admin: header = get_colored_header('ИНИЦИАТИВНАЯ ГРУППА', 'ig' if is_member else 'admin') text = f'{header}\n\n' text += f'{get_level_badge("admin" if is_admin else "ig")}\n\n' if is_admin and not is_member: text += '🟡 Вы — администратор!\n\n' text += 'У вас есть полный доступ к функциям ИГ.\n' text += 'Для вступления в ИГ используйте:\n' text += '/ig_add 197957361 admin\n\n' else: text += 'Вы — участник инициативной группы!\n\n' text += 'Ваши возможности:\n' text += '• 📊 Статистика активности\n' text += '• 📋 Список участников\n' text += '• 👤 Мой профиль\n' text += '• 📣 Рассылка для ИГ\n' text += '• 📢 Обращения к администрации\n' text += '• 🗳️ Голосования ИГ\n\n' text += 'Больше функций в разработке...' builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text='🔵 📊 Моя статистика', callback_data='ig_my_stats'), ) builder.row( InlineKeyboardButton(text='🔵 📋 Список ИГ', callback_data='ig_list_members'), InlineKeyboardButton(text='🔵 👤 Мой профиль', callback_data='ig_my_profile'), ) builder.row( InlineKeyboardButton(text='🔵 📣 Рассылка ИГ', callback_data='ig_broadcast'), ) builder.row( InlineKeyboardButton(text='🔙 Назад', callback_data='main_menu'), ) await callback.message.answer( text, reply_markup=builder.as_markup(), parse_mode='HTML' ) else: await callback.message.answer( 'ℹ️ Инициативная группа\n\n' 'Вы не являетесь участником ИГ.\n\n' 'Инициативная группа — это активные жильцы,\n' 'которые помогают управлять домом.\n\n' 'Чтобы вступить, обратитесь к администратору.', parse_mode='HTML' ) # ============================================================================= # FSM для добавления в ИГ # ============================================================================= class IGState(StatesGroup): waiting_for_notes = State() waiting_for_broadcast = State() waiting_for_broadcast_file = State() # ============================================================================= # РАССЫЛКА ДЛЯ ИГ # ============================================================================= @router.callback_query(F.data == 'ig_broadcast') async def cb_ig_broadcast(callback: CallbackQuery): """Рассылка для ИГ""" from aiogram.types import InputFile await callback.answer() await callback.message.answer( '📣 Рассылка для Инициативной Группы\n\n' 'Отправьте текст сообщения:\n\n' 'Или прикрепите файл (PDF, фото и т.д.)\n\n' 'Отправьте "отмена" для выхода.', parse_mode='HTML' ) await callback.from_user.id await callback.message.bot.get_updates() await callback.message.answer('👇 Отправьте текст или файл:') # Проще через команду await callback.message.answer( 'ℹ️ Для отправки рассылки используйте команду:\n\n' '/ig_broadcast\n\n' 'Бот попросит ввести текст и файл.', parse_mode='HTML' ) class IGState(StatesGroup): waiting_for_notes = State() # ============================================================================= # ДОБАВЛЕНИЕ В ИГ # ============================================================================= @router.message(Command('ig_add')) @admin_only async def cmd_ig_add(message: Message, state: FSMContext): """ Добавить пользователя в ИГ Использование: /ig_add [роль] Роли: member, admin, coordinator """ args = message.text.split() if len(args) < 2: await message.answer( '❌ Использование:\n' '/ig_add <user_id> [роль]\n\n' 'Роли: member, admin, coordinator\n' 'Пример: /ig_add 123456789 member', parse_mode='HTML' ) return try: user_id = int(args[1]) except ValueError: await message.answer('❌ Неверный ID пользователя') return role = args[2] if len(args) > 2 else 'member' valid_roles = ['member', 'admin', 'coordinator'] if role not in valid_roles: await message.answer(f'❌ Неверная роль. Доступные: {", ".join(valid_roles)}') return async with AsyncSessionLocal() as session: ig_service = InitiativeGroupService(session) success = await ig_service.add_member(user_id, role) if success: # Получаем имя пользователя from database.models import User user = await session.get(User, user_id) name = user.full_name or user.username or f'user{user_id}' if user else f'user{user_id}' await message.answer( f'✅ Пользователь добавлен в ИГ!\n\n' f'👤 {name}\n' f'🔖 ID: {user_id}\n' f'🎭 Роль: {role}\n\n' f'Отправьте заметку о роли (необязательно) или "пропустить":', parse_mode='HTML' ) await state.update_data(ig_user_id=user_id) await state.set_state(IGState.waiting_for_notes) else: await message.answer('❌ Не удалось добавить участника') @router.message(IGState.waiting_for_notes) async def process_ig_notes(message: Message, state: FSMContext): """Получили заметку о роли""" data = await state.get_data() user_id = data.get('ig_user_id') notes = message.text if message.text.lower() != 'пропустить' else None if notes and notes.lower() != 'пропустить': async with AsyncSessionLocal() as session: ig_service = InitiativeGroupService(session) await ig_service.update_role(user_id, 'member') await message.answer('✅ Заметка сохранена') await state.clear() # ============================================================================= # СТАТИСТИКА УЧАСТНИКА ИГ # ============================================================================= @router.callback_query(F.data == 'ig_my_stats') async def cb_ig_my_stats(callback: CallbackQuery): """Статистика участника ИГ""" from database.models import User await callback.answer() user_id = callback.from_user.id async with AsyncSessionLocal() as session: ig_service = InitiativeGroupService(session) member_info = await ig_service.get_member(user_id) user = await session.get(User, user_id) if member_info and user: text = '📊 Ваша статистика в ИГ\n\n' text += f'👤 {user.full_name or user.username}\n' text += f'🏠 Квартира: {user.apartment or "не указана"}\n\n' text += f'🎭 Роль: {member_info["role"]}\n' text += f'📅 В группе с: {member_info["joined_at"].strftime("%d.%m.%Y")}\n\n' text += f'📊 Ваша активность:\n' text += f'• Сообщений: {user.message_count}\n' text += f'• Рейтинг: {user.rating}\n' text += f'• Уровень: {user.level}\n' await callback.message.answer(text, parse_mode='HTML') else: await callback.message.answer('❌ Вы не являетесь участником ИГ') # ============================================================================= # СПИСОК УЧАСТНИКОВ ИГ # ============================================================================= @router.callback_query(F.data == 'ig_list_members') async def cb_ig_list_members(callback: CallbackQuery): """Список участников ИГ""" await callback.answer() async with AsyncSessionLocal() as session: ig_service = InitiativeGroupService(session) members = await ig_service.get_members(active_only=True) if members: text = '📋 Участники инициативной группы\n\n' # Группируем по ролям by_role = {} for m in members: role = m['role'] if role not in by_role: by_role[role] = [] by_role[role].append(m) role_names = { 'admin': '👑 Админы', 'coordinator': '🎯 Координаторы', 'member': '👥 Участники' } for role, role_members in by_role.items(): text += f'\n{role_names.get(role, role)}\n' for m in role_members[:10]: # Макс 10 на роль apt = f' (кв. {m["apartment"]})' if m.get('apartment') else '' text += f'• {m["name"]}{apt}\n' if len(members) > 30: text += f'\n... и ещё {len(members) - 30} участников' await callback.message.answer(text, parse_mode='HTML') else: await callback.message.answer('ℹ️ В ИГ пока нет участников') # ============================================================================= # МОЙ ПРОФИЛЬ (РЕДАКТИРОВАНИЕ) # ============================================================================= @router.callback_query(F.data == 'ig_my_profile') async def cb_ig_my_profile(callback: CallbackQuery): """Редактирование профиля участника ИГ""" from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from database.models import User await callback.answer() user_id = callback.from_user.id async with AsyncSessionLocal() as session: user = await session.get(User, user_id) if user: phone_text = f'📞 Телефон: {user.phone or "не указан"}\n' apt_text = f'🏠 Квартира: {user.apartment or "не указана"}\n' text = '👤 Ваш профиль\n\n' text += apt_text text += phone_text text += '\nЧто хотите изменить?' builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text='🏠 Изменить квартиру', callback_data='ig_edit_apt'), ) builder.row( InlineKeyboardButton(text='📞 Изменить телефон', callback_data='ig_edit_phone'), ) builder.row( InlineKeyboardButton(text='🔙 Назад', callback_data='ig_menu'), ) await callback.message.answer( text, reply_markup=builder.as_markup(), parse_mode='HTML' ) else: await callback.message.answer('❌ Вы не найдены в базе') # ============================================================================= # ИЗМЕНИТЬ КВАРТИРУ # ============================================================================= @router.callback_query(F.data == 'ig_edit_apt') async def cb_ig_edit_apt(callback: CallbackQuery): """Изменение квартиры""" await callback.answer() await callback.message.answer( '🏠 Изменение квартиры\n\n' 'ℹ️ Используйте команду:\n\n' '/setapt 45\n\n' 'Где 45 — ваш номер квартиры.\n\n' 'Или нажмите "Назад".', parse_mode='HTML' ) # ============================================================================= # ИЗМЕНИТЬ ТЕЛЕФОН # ============================================================================= @router.callback_query(F.data == 'ig_edit_phone') async def cb_ig_edit_phone(callback: CallbackQuery): """Изменение телефона""" await callback.answer() await callback.message.answer( '📞 Изменение телефона\n\n' 'ℹ️ Используйте команду:\n\n' '/setphone +79991234567\n\n' 'Или нажмите "Назад".', parse_mode='HTML' ) # ============================================================================= # УДАЛЕНИЕ ИЗ ИГ # ============================================================================= @router.message(Command('ig_remove')) @admin_only async def cmd_ig_remove(message: Message): """ Удалить пользователя из ИГ Использование: /ig_remove """ args = message.text.split() if len(args) < 2: await message.answer( '❌ Использование:\n' '/ig_remove <user_id>', parse_mode='HTML' ) return try: user_id = int(args[1]) except ValueError: await message.answer('❌ Неверный ID пользователя') return async with AsyncSessionLocal() as session: ig_service = InitiativeGroupService(session) success = await ig_service.remove_member(user_id) if success: await message.answer(f'✅ Пользователь {user_id} удалён из ИГ') else: await message.answer(f'❌ Пользователь {user_id} не найден в ИГ') # ============================================================================= # СПИСОК УЧАСТНИКОВ ИГ # ============================================================================= @router.message(Command('ig_list')) @admin_only async def cmd_ig_list(message: Message): """Показать список участников ИГ""" async with AsyncSessionLocal() as session: ig_service = InitiativeGroupService(session) members = await ig_service.get_members(active_only=True) stats = await ig_service.get_stats() if not members: await message.answer('ℹ️ В инициативной группе пока нет участников') return text = f'👥 Инициативная группа\n\n' text += f'Всего участников: {stats["active"]}\n\n' # Группируем по ролям by_role = {} for member in members: role = member['role'] if role not in by_role: by_role[role] = [] by_role[role].append(member) role_names = { 'admin': '🔴 Администраторы', 'coordinator': '🟡 Координаторы', 'member': '🟢 Участники' } for role, role_members in by_role.items(): role_name = role_names.get(role, role.capitalize()) text += f'{role_name}\n' for m in role_members: apt = f'(кв. {m["apartment"]})' if m['apartment'] else '' text += f' • {m["name"]} {apt}\n' text += '\n' await message.answer(text, parse_mode='HTML') # ============================================================================= # СТАТИСТИКА ИГ # ============================================================================= @router.message(Command('ig_stats')) @admin_only async def cmd_ig_stats(message: Message): """Статистика ИГ""" async with AsyncSessionLocal() as session: ig_service = InitiativeGroupService(session) stats = await ig_service.get_stats() text = '📊 Статистика инициативной группы\n\n' text += f'Всего участников: {stats["total"]}\n' text += f'Активных: {stats["active"]}\n\n' if stats['by_role']: text += 'По ролям:\n' for role, count in stats['by_role'].items(): text += f' • {role}: {count}\n' await message.answer(text, parse_mode='HTML') # ============================================================================= # РАССЫЛКА ТОЛЬКО ИГ # ============================================================================= @router.message(Command('ig_broadcast')) @admin_only async def cmd_ig_broadcast(message: Message, state: FSMContext): """ Рассылка только участникам ИГ Использование: /ig_broadcast """ await message.answer( '📬 Рассылка для инициативной группы\n\n' 'Отправьте сообщение для рассылки:', parse_mode='HTML' ) await state.set_state('ig_broadcast_waiting') @router.message(StateFilter('ig_broadcast_waiting')) @admin_only async def process_ig_broadcast(message: Message, state: FSMContext): """Отправка рассылки ИГ""" broadcast_message = message.text async with AsyncSessionLocal() as session: ig_service = InitiativeGroupService(session) member_ids = await ig_service.get_ig_only_broadcast_recipients() if not member_ids: await message.answer('❌ В ИГ нет участников') await state.clear() return success_count = 0 error_count = 0 for user_id in member_ids: try: await message.bot.send_message( user_id, f'📬 Сообщение для инициативной группы\n\n' f'{broadcast_message}\n\n' f'Это сообщение отправлено только участникам ИГ', parse_mode='HTML' ) success_count += 1 except Exception as e: logger.error(f'Не удалось отправить сообщение пользователю {user_id}: {e}') error_count += 1 await message.answer( f'✅ Рассылка завершена!\n\n' f'Успешно: {success_count}\n' f'Ошибок: {error_count}\n' f'Получателей: {len(member_ids)}', parse_mode='HTML' ) await state.clear() # ============================================================================= # ПРОВЕРКА УЧАСТНИКА # ============================================================================= @router.message(Command('ig_check')) @admin_only async def cmd_ig_check(message: Message): """ Проверить является ли пользователь участником ИГ Использование: /ig_check """ args = message.text.split() if len(args) < 2: await message.answer( '❌ Использование:\n' '/ig_check <user_id>', parse_mode='HTML' ) return try: user_id = int(args[1]) except ValueError: await message.answer('❌ Неверный ID пользователя') return async with AsyncSessionLocal() as session: ig_service = InitiativeGroupService(session) member = await ig_service.get_member(user_id) if member: from database.models import User user = await session.get(User, user_id) name = user.full_name or user.username or f'user{user_id}' if user else f'user{user_id}' await message.answer( f'✅ Пользователь в ИГ\n\n' f'👤 {name}\n' f'🎭 Роль: {member["role"]}\n' f'📅 В ИГ с: {member["joined_at"].strftime("%d.%m.%Y")}\n' f'📝 Заметка: {member["notes"] or "нет"}', parse_mode='HTML' ) else: await message.answer(f'ℹ️ Пользователь {user_id} не является участником ИГ')