domovoy_bot/handlers/initiative_group.py
Admin 19c5fdd0ac feat: v2.1 - Инициативная группа
 Модель InitiativeGroup в БД
 Сервис initiative_group.py
 Хендлеры: /ig_add, /ig_remove, /ig_list, /ig_stats, /ig_check, /ig_broadcast
 Декораторы для ИГ
 Миграция v2.1 выполнена

📦 Бекап: backups/versions/v2.1_initiative_group_2026-02-27/

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-28 10:11:36 +00:00

320 lines
12 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Initiative Group — хендлеры для работы с инициативной группой
Только для администраторов
"""
import logging
from aiogram import Router, F
from aiogram.types import Message
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()
# =============================================================================
# FSM для добавления в ИГ
# =============================================================================
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 <user_id> [роль]
Роли: member, admin, coordinator
"""
args = message.text.split()
if len(args) < 2:
await message.answer(
'❌ <b>Использование:</b>\n'
'<code>/ig_add &lt;user_id&gt; [роль]</code>\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'✅ <b>Пользователь добавлен в ИГ!</b>\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') # Можно расширить для сохранения notes
await message.answer('✅ Заметка сохранена')
await state.clear()
# =============================================================================
# УДАЛЕНИЕ ИЗ ИГ
# =============================================================================
@router.message(Command('ig_remove'))
@admin_only
async def cmd_ig_remove(message: Message):
"""
Удалить пользователя из ИГ
Использование: /ig_remove <user_id>
"""
args = message.text.split()
if len(args) < 2:
await message.answer(
'❌ <b>Использование:</b>\n'
'<code>/ig_remove &lt;user_id&gt;</code>',
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'👥 <b>Инициативная группа</b>\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'<b>{role_name}</b>\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 = '📊 <b>Статистика инициативной группы</b>\n\n'
text += f'Всего участников: {stats["total"]}\n'
text += f'Активных: {stats["active"]}\n\n'
if stats['by_role']:
text += '<b>По ролям:</b>\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(
'📬 <b>Рассылка для инициативной группы</b>\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'📬 <b>Сообщение для инициативной группы</b>\n\n'
f'{broadcast_message}\n\n'
f'<i>Это сообщение отправлено только участникам ИГ</i>',
parse_mode='HTML'
)
success_count += 1
except Exception as e:
logger.error(f'Не удалось отправить сообщение пользователю {user_id}: {e}')
error_count += 1
await message.answer(
f'✅ <b>Рассылка завершена!</b>\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 <user_id>
"""
args = message.text.split()
if len(args) < 2:
await message.answer(
'❌ <b>Использование:</b>\n'
'<code>/ig_check &lt;user_id&gt;</code>',
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'✅ <b>Пользователь в ИГ</b>\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} не является участником ИГ')