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>
This commit is contained in:
parent
32f6ca8d5d
commit
19c5fdd0ac
7 changed files with 637 additions and 3 deletions
49
database/migrate_add_initiative_group.py
Normal file
49
database/migrate_add_initiative_group.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"""
|
||||
Миграция v2.1: создать таблицу initiative_group для инициативной группы
|
||||
"""
|
||||
import sqlite3
|
||||
import asyncio
|
||||
|
||||
DB_PATH = 'database/domovoy.db'
|
||||
|
||||
|
||||
async def migrate():
|
||||
"""Создать таблицу initiative_group"""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Проверяем, есть ли уже таблица initiative_group
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='initiative_group'")
|
||||
existing = cursor.fetchone()
|
||||
|
||||
if existing:
|
||||
print("ℹ️ Таблица initiative_group уже существует")
|
||||
else:
|
||||
print("➕ Создаём таблицу initiative_group...")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE initiative_group (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id BIGINT NOT NULL UNIQUE,
|
||||
role TEXT DEFAULT 'member',
|
||||
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
notes TEXT,
|
||||
FOREIGN KEY (user_id) REFERENCES users(user_id)
|
||||
)
|
||||
""")
|
||||
|
||||
# Создаём индексы
|
||||
cursor.execute("CREATE INDEX ix_initiative_group_user ON initiative_group(user_id)")
|
||||
cursor.execute("CREATE INDEX ix_initiative_group_active ON initiative_group(is_active)")
|
||||
|
||||
print("✅ Таблица initiative_group создана")
|
||||
print("✅ Индексы созданы")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("✅ Миграция v2.1 завершена!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(migrate())
|
||||
|
|
@ -571,3 +571,26 @@ class EventParticipant(Base):
|
|||
|
||||
def __repr__(self):
|
||||
return f"<EventParticipant event={self.event_id} user={self.user_id}>"
|
||||
|
||||
|
||||
class InitiativeGroup(Base):
|
||||
"""Участники инициативной группы"""
|
||||
__tablename__ = 'initiative_group'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False, unique=True)
|
||||
role = Column(String(50), default='member') # admin, member, coordinator
|
||||
joined_at = Column(DateTime, default=datetime.utcnow)
|
||||
is_active = Column(Boolean, default=True)
|
||||
notes = Column(Text, nullable=True) # Заметки о роли
|
||||
|
||||
# Связи
|
||||
user = relationship('User', backref='initiative_membership')
|
||||
|
||||
__table_args__ = (
|
||||
Index('ix_initiative_group_user', 'user_id'),
|
||||
Index('ix_initiative_group_active', 'is_active'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<InitiativeGroup user={self.user_id} role={self.role}>"
|
||||
|
|
|
|||
|
|
@ -22,3 +22,4 @@ from .profile_history import router as profile_history_router
|
|||
from .achievements import router as achievements_router
|
||||
from .thanks import router as thanks_router
|
||||
from .events import router as events_router
|
||||
from .initiative_group import router as initiative_group_router
|
||||
|
|
|
|||
320
handlers/initiative_group.py
Normal file
320
handlers/initiative_group.py
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
"""
|
||||
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} не является участником ИГ')
|
||||
14
main.py
14
main.py
|
|
@ -121,7 +121,16 @@
|
|||
|
||||
🎉 ВСЕ ФУНКЦИИ v1.x РЕАЛИЗОВАНЫ! 🎉
|
||||
|
||||
ПЛАН РАЗРАБОТКИ (v2.0 — AI-Юрист):
|
||||
РЕАЛИЗОВАНО В v2.1 (27.02.2026):
|
||||
- ✅ Инициативная группа (отдельная сущность от админов)
|
||||
- ✅ Модель InitiativeGroup в БД
|
||||
- ✅ Команды: /ig_add, /ig_remove, /ig_list, /ig_stats, /ig_check
|
||||
- ✅ Рассылка только для ИГ: /ig_broadcast
|
||||
- ✅ Декораторы для ИГ
|
||||
|
||||
🚀 НАЧАТА РАЗРАБОТКА v2.x! 🚀
|
||||
|
||||
ПЛАН РАЗРАБОТКИ (v2.2 — v2.x):
|
||||
См. файл DEVELOPMENT_PLAN.md и VERSIONS.md
|
||||
|
||||
БЕКАПЫ ВЕРСИЙ:
|
||||
|
|
@ -151,7 +160,7 @@ from config import (
|
|||
USE_PROXY, PROXY_TYPE, PROXY_HOST, PROXY_PORT
|
||||
)
|
||||
from database.db import init_db
|
||||
from handlers import admin_router, users_router, verification_router, chat_monitor_router, security_router, apartments_router, antimat_router, phones_router, polls_router, antispam_router, ratelimit_router, schedule_router, ads_router, payments_router, multicount_router, topics_router, toxicity_router, profile_history_router, achievements_router, thanks_router, events_router
|
||||
from handlers import admin_router, users_router, verification_router, chat_monitor_router, security_router, apartments_router, antimat_router, phones_router, polls_router, antispam_router, ratelimit_router, schedule_router, ads_router, payments_router, multicount_router, topics_router, toxicity_router, profile_history_router, achievements_router, thanks_router, events_router, initiative_group_router
|
||||
from services.scheduler import Scheduler
|
||||
from services.web_server import WebServerService
|
||||
|
||||
|
|
@ -276,6 +285,7 @@ async def main():
|
|||
dp.include_router(achievements_router)
|
||||
dp.include_router(thanks_router)
|
||||
dp.include_router(events_router)
|
||||
dp.include_router(initiative_group_router)
|
||||
|
||||
# Регистрируем хуки
|
||||
dp.startup.register(on_startup)
|
||||
|
|
|
|||
189
services/initiative_group.py
Normal file
189
services/initiative_group.py
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
"""
|
||||
Initiative Group Service — сервис для работы с инициативной группой
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InitiativeGroupService:
|
||||
"""Сервис для управления инициативной группой"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def add_member(self, user_id: int, role: str = 'member', notes: str = None) -> bool:
|
||||
"""
|
||||
Добавить участника в ИГ
|
||||
Returns: True если успешно
|
||||
"""
|
||||
from database.models import InitiativeGroup
|
||||
|
||||
# Проверяем не состоит ли уже
|
||||
existing = await self.session.get(InitiativeGroup, user_id)
|
||||
if existing:
|
||||
# Обновляем роль и статус
|
||||
existing.role = role
|
||||
existing.is_active = True
|
||||
if notes:
|
||||
existing.notes = notes
|
||||
await self.session.commit()
|
||||
logger.info(f"Участник {user_id} обновлён в ИГ (роль: {role})")
|
||||
return True
|
||||
|
||||
# Создаём нового
|
||||
member = InitiativeGroup(
|
||||
user_id=user_id,
|
||||
role=role,
|
||||
notes=notes
|
||||
)
|
||||
self.session.add(member)
|
||||
await self.session.commit()
|
||||
|
||||
logger.info(f"Участник {user_id} добавлен в ИГ (роль: {role})")
|
||||
return True
|
||||
|
||||
async def remove_member(self, user_id: int) -> bool:
|
||||
"""
|
||||
Удалить участника из ИГ
|
||||
Returns: True если успешно
|
||||
"""
|
||||
from database.models import InitiativeGroup
|
||||
|
||||
member = await self.session.get(InitiativeGroup, user_id)
|
||||
if not member:
|
||||
return False
|
||||
|
||||
# Не удаляем, а деактивируем
|
||||
member.is_active = False
|
||||
await self.session.commit()
|
||||
|
||||
logger.info(f"Участник {user_id} удалён из ИГ")
|
||||
return True
|
||||
|
||||
async def get_members(self, active_only: bool = True) -> List[Dict]:
|
||||
"""
|
||||
Получить список участников ИГ
|
||||
Returns: список участников
|
||||
"""
|
||||
from database.models import InitiativeGroup, User
|
||||
|
||||
stmt = select(InitiativeGroup).join(User, InitiativeGroup.user_id == User.user_id)
|
||||
|
||||
if active_only:
|
||||
stmt = stmt.where(InitiativeGroup.is_active == True)
|
||||
|
||||
stmt = stmt.order_by(InitiativeGroup.joined_at)
|
||||
result = await self.session.execute(stmt)
|
||||
members = list(result.scalars().all())
|
||||
|
||||
return [
|
||||
{
|
||||
'user_id': m.user_id,
|
||||
'role': m.role,
|
||||
'joined_at': m.joined_at,
|
||||
'is_active': m.is_active,
|
||||
'notes': m.notes,
|
||||
'name': m.user.full_name or m.user.username or f'user{m.user_id}',
|
||||
'apartment': m.user.apartment
|
||||
}
|
||||
for m in members
|
||||
]
|
||||
|
||||
async def get_member(self, user_id: int) -> Optional[Dict]:
|
||||
"""
|
||||
Получить информацию об участнике ИГ
|
||||
Returns: информация или None
|
||||
"""
|
||||
from database.models import InitiativeGroup
|
||||
|
||||
member = await self.session.get(InitiativeGroup, user_id)
|
||||
if not member or not member.is_active:
|
||||
return None
|
||||
|
||||
return {
|
||||
'user_id': member.user_id,
|
||||
'role': member.role,
|
||||
'joined_at': member.joined_at,
|
||||
'notes': member.notes
|
||||
}
|
||||
|
||||
async def is_member(self, user_id: int) -> bool:
|
||||
"""
|
||||
Проверить является ли пользователь участником ИГ
|
||||
Returns: True если участник
|
||||
"""
|
||||
from database.models import InitiativeGroup
|
||||
|
||||
member = await self.session.get(InitiativeGroup, user_id)
|
||||
return member is not None and member.is_active
|
||||
|
||||
async def update_role(self, user_id: int, role: str) -> bool:
|
||||
"""
|
||||
Обновить роль участника
|
||||
Returns: True если успешно
|
||||
"""
|
||||
from database.models import InitiativeGroup
|
||||
|
||||
member = await self.session.get(InitiativeGroup, user_id)
|
||||
if not member:
|
||||
return False
|
||||
|
||||
member.role = role
|
||||
await self.session.commit()
|
||||
|
||||
logger.info(f"Роль участника {user_id} изменена на {role}")
|
||||
return True
|
||||
|
||||
async def get_member_ids(self, active_only: bool = True) -> List[int]:
|
||||
"""
|
||||
Получить список ID участников ИГ
|
||||
Returns: список ID
|
||||
"""
|
||||
from database.models import InitiativeGroup
|
||||
|
||||
stmt = select(InitiativeGroup.user_id)
|
||||
if active_only:
|
||||
stmt = stmt.where(InitiativeGroup.is_active == True)
|
||||
|
||||
result = await self.session.execute(stmt)
|
||||
return [row[0] for row in result.all()]
|
||||
|
||||
async def get_stats(self) -> Dict:
|
||||
"""
|
||||
Получить статистику ИГ
|
||||
Returns: статистика
|
||||
"""
|
||||
from database.models import InitiativeGroup
|
||||
|
||||
# Всего участников
|
||||
stmt_total = select(func.count(InitiativeGroup.id))
|
||||
result = await self.session.execute(stmt_total)
|
||||
total = result.scalar() or 0
|
||||
|
||||
# Активных
|
||||
stmt_active = select(func.count(InitiativeGroup.id)).where(InitiativeGroup.is_active == True)
|
||||
result = await self.session.execute(stmt_active)
|
||||
active = result.scalar() or 0
|
||||
|
||||
# По ролям
|
||||
stmt_roles = select(InitiativeGroup.role, func.count(InitiativeGroup.id)).group_by(InitiativeGroup.role)
|
||||
result = await self.session.execute(stmt_roles)
|
||||
by_role = {row[0]: row[1] for row in result.all()}
|
||||
|
||||
return {
|
||||
'total': total,
|
||||
'active': active,
|
||||
'by_role': by_role
|
||||
}
|
||||
|
||||
async def get_ig_only_broadcast_recipients(self) -> List[int]:
|
||||
"""
|
||||
Получить список ID для рассылки только ИГ
|
||||
Returns: список user_id
|
||||
"""
|
||||
return await self.get_member_ids(active_only=True)
|
||||
|
|
@ -26,6 +26,29 @@ def admin_only(func):
|
|||
return wrapper
|
||||
|
||||
|
||||
def initiative_group_only(func):
|
||||
"""Только для участников инициативной группы"""
|
||||
@wraps(func)
|
||||
async def wrapper(message: Message, *args, **kwargs):
|
||||
from database.db import AsyncSessionLocal
|
||||
from services.initiative_group import InitiativeGroupService
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
ig_service = InitiativeGroupService(session)
|
||||
is_member = await ig_service.is_member(message.from_user.id)
|
||||
|
||||
# Также разрешаем админам
|
||||
is_admin = config.is_admin(message.from_user.id)
|
||||
|
||||
if not (is_member or is_admin):
|
||||
await message.answer(
|
||||
'❌ Эта команда доступна только участникам инициативной группы'
|
||||
)
|
||||
return
|
||||
return await func(message, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def verified_only(func):
|
||||
"""Только для верифицированных жильцов"""
|
||||
@wraps(func)
|
||||
|
|
@ -56,3 +79,22 @@ def admin_callback_only(func):
|
|||
return
|
||||
return await func(callback, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def ig_callback_only(func):
|
||||
"""Только для участников ИГ (callback queries)"""
|
||||
@wraps(func)
|
||||
async def wrapper(callback: CallbackQuery, *args, **kwargs):
|
||||
from database.db import AsyncSessionLocal
|
||||
from services.initiative_group import InitiativeGroupService
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
ig_service = InitiativeGroupService(session)
|
||||
is_member = await ig_service.is_member(callback.from_user.id)
|
||||
is_admin = config.is_admin(callback.from_user.id)
|
||||
|
||||
if not (is_member or is_admin):
|
||||
await callback.answer('❌ Только для участников ИГ', show_alert=True)
|
||||
return
|
||||
return await func(callback, *args, **kwargs)
|
||||
return wrapper
|
||||
|
|
|
|||
Loading…
Reference in a new issue