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):
|
def __repr__(self):
|
||||||
return f"<EventParticipant event={self.event_id} user={self.user_id}>"
|
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 .achievements import router as achievements_router
|
||||||
from .thanks import router as thanks_router
|
from .thanks import router as thanks_router
|
||||||
from .events import router as events_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 РЕАЛИЗОВАНЫ! 🎉
|
🎉 ВСЕ ФУНКЦИИ 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
|
См. файл DEVELOPMENT_PLAN.md и VERSIONS.md
|
||||||
|
|
||||||
БЕКАПЫ ВЕРСИЙ:
|
БЕКАПЫ ВЕРСИЙ:
|
||||||
|
|
@ -151,7 +160,7 @@ from config import (
|
||||||
USE_PROXY, PROXY_TYPE, PROXY_HOST, PROXY_PORT
|
USE_PROXY, PROXY_TYPE, PROXY_HOST, PROXY_PORT
|
||||||
)
|
)
|
||||||
from database.db import init_db
|
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.scheduler import Scheduler
|
||||||
from services.web_server import WebServerService
|
from services.web_server import WebServerService
|
||||||
|
|
||||||
|
|
@ -276,6 +285,7 @@ async def main():
|
||||||
dp.include_router(achievements_router)
|
dp.include_router(achievements_router)
|
||||||
dp.include_router(thanks_router)
|
dp.include_router(thanks_router)
|
||||||
dp.include_router(events_router)
|
dp.include_router(events_router)
|
||||||
|
dp.include_router(initiative_group_router)
|
||||||
|
|
||||||
# Регистрируем хуки
|
# Регистрируем хуки
|
||||||
dp.startup.register(on_startup)
|
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
|
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):
|
def verified_only(func):
|
||||||
"""Только для верифицированных жильцов"""
|
"""Только для верифицированных жильцов"""
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
|
|
@ -56,3 +79,22 @@ def admin_callback_only(func):
|
||||||
return
|
return
|
||||||
return await func(callback, *args, **kwargs)
|
return await func(callback, *args, **kwargs)
|
||||||
return wrapper
|
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