"""
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} не является участником ИГ')