✅ Модель 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>
320 lines
12 KiB
Python
320 lines
12 KiB
Python
"""
|
||
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 <user_id> [роль]</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 <user_id></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 <user_id></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} не является участником ИГ')
|