domovoy_bot/handlers/digest.py
Admin 222c2ba521 🚀 feat: V3.1 - Smart broadcasts with read tracking + Weekly digests with approval workflow
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-06 23:01:10 +04:00

319 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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.

"""
Обработчики для еженедельного дайджеста
"""
import logging
from datetime import datetime
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from config import ADMIN_USER_ID
from database.db import AsyncSessionLocal
from database.models import Digest
from services.digest_service import DigestService
logger = logging.getLogger(__name__)
router = Router()
# ============================================================================
# КОМАНДЫ ДЛЯ АДМИНА
# ============================================================================
@router.message(F.text == '📰 Сгенерировать дайджест')
@router.message(F.text.startswith('/digest'))
async def cmd_digest(message: Message):
"""Сгенерировать дайджест за текущую неделю"""
if message.from_user.id != ADMIN_USER_ID:
await message.answer("❌ Эта команда только для администратора")
return
await message.answer("⏳ Генерация дайджеста...")
try:
async with AsyncSessionLocal() as session:
service = DigestService(session)
# Генерируем
digest_data = await service.generate_weekly_digest()
if not digest_data:
await message.answer("Не удалось сгенерировать дайджест")
return
# Сохраняем со статусом pending_approval
digest_id = await service.save_digest(digest_data, status='pending_approval')
# Отправляем превью админу
preview_text = digest_data['text']
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="✅ Утвердить и отправить", callback_data=f"digest_approve:{digest_id}")],
[InlineKeyboardButton(text="❌ Отклонить", callback_data=f"digest_reject:{digest_id}")],
[InlineKeyboardButton(text="✏️ Редактировать", callback_data=f"digest_edit:{digest_id}")],
[InlineKeyboardButton(text="🌐 Открыть в веб-панели", url="http://localhost:8000/digests")],
])
await message.answer(
f"📰 <b>Дайджест #{digest_data['week_number']}/{digest_data['year']}</b>\n"
f"🗓 Период: {digest_data['period_start'].strftime('%d.%m')} - {digest_data['period_end'].strftime('%d.%m.%Y')}\n\n"
f"{preview_text}\n\n"
f"⏳ Ожидает вашего утверждения",
parse_mode='HTML',
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Ошибка генерации дайджеста: {e}")
await message.answer(f"❌ Ошибка: {e}")
@router.callback_query(F.data.startswith('digest_approve:'))
async def approve_digest(callback: CallbackQuery):
"""Утвердить и отправить дайджест"""
if callback.from_user.id != ADMIN_USER_ID:
return
digest_id = int(callback.data.split(':')[1])
await callback.message.edit_text(f"✅ Дайджест утверждён! Отправляем...")
try:
async with AsyncSessionLocal() as session:
service = DigestService(session)
# Обновляем статус
await service.update_digest_status(
digest_id,
'approved',
approved_by=callback.from_user.id
)
# Получаем дайджест
digest = await session.get(Digest, digest_id)
if not digest:
await callback.message.edit_text("❌ Дайджест не найден")
return
# Отправляем всем верифицированным
from database.models import User
stmt = select(User).where(User.verified == True)
result = await session.execute(stmt)
users = list(result.scalars().all())
sent_count = 0
error_count = 0
message_id = None
# Сначала в чат
try:
chat_msg = await callback.bot.send_message(
callback.message.chat.id if hasattr(callback.message, 'chat') else -1002608277768,
f"📰 <b>Еженедельный дайджест</b>\n\n{digest.digest_text}",
parse_mode='HTML'
)
message_id = chat_msg.message_id
except Exception as e:
logger.error(f"Ошибка отправки в чат: {e}")
# Потом пользователям
for user in users:
try:
await callback.bot.send_message(
user.user_id,
f"📰 <b>Еженедельный дайджест</b>\n\n{digest.digest_text}",
parse_mode='HTML'
)
sent_count += 1
except Exception as e:
logger.error(f"Ошибка отправки пользователю {user.user_id}: {e}")
error_count += 1
# Обновляем статус на sent
await service.update_digest_status(
digest_id,
'sent',
message_id=message_id
)
# Уведомляем админа
await callback.message.edit_text(
f"✅ <b>Дайджест отправлен!</b>\n\n"
f"ID: #{digest_id}\n"
f"Получателей: {sent_count}\n"
f"Ошибок: {error_count}\n"
f"Время: {datetime.utcnow().strftime('%H:%M:%S')}",
parse_mode='HTML'
)
except Exception as e:
logger.error(f"Ошибка отправки дайджеста: {e}")
await callback.message.edit_text(f"❌ Ошибка: {e}")
@router.callback_query(F.data.startswith('digest_reject:'))
async def reject_digest(callback: CallbackQuery, state: FSMContext):
"""Отклонить дайджест"""
if callback.from_user.id != ADMIN_USER_ID:
return
digest_id = int(callback.data.split(':')[1])
await state.update_data(reject_digest_id=digest_id)
await callback.message.edit_text(
"✏️ Введите причину отклонения (или /cancel для отмены):"
)
from aiogram.fsm.state import StatesGroup, State
class RejectState(StatesGroup):
waiting_reason = State()
await state.set_state(RejectState.waiting_reason)
@router.message(F.text == '/cancel')
async def cancel_action(message: Message, state: FSMContext):
"""Отменить действие"""
await message.answer("❌ Действие отменено")
await state.clear()
@router.message(F.state == 'RejectState:waiting_reason')
async def process_reject_reason(message: Message, state: FSMContext):
"""Обработка причины отклонения"""
from aiogram.fsm.state import StatesGroup, State
class RejectState(StatesGroup):
waiting_reason = State()
reason = message.text
data = await state.get_data()
digest_id = data.get('reject_digest_id')
try:
async with AsyncSessionLocal() as session:
service = DigestService(session)
await service.update_digest_status(
digest_id,
'rejected',
rejected_by=message.from_user.id,
rejection_reason=reason
)
await message.answer(f"❌ Дайджест #{digest_id} отклонён\nПричина: {reason}")
except Exception as e:
await message.answer(f"❌ Ошибка: {e}")
await state.clear()
@router.callback_query(F.data.startswith('digest_edit:'))
async def edit_digest(callback: CallbackQuery, state: FSMContext):
"""Редактировать текст дайджеста"""
if callback.from_user.id != ADMIN_USER_ID:
return
digest_id = int(callback.data.split(':')[1])
await state.update_data(edit_digest_id=digest_id)
await callback.message.edit_text(
"✏️ Отредактируйте текст дайджеста и отправьте его сообщением.\n\n"
"Или нажмите /cancel для отмены."
)
from aiogram.fsm.state import StatesGroup, State
class EditDigestState(StatesGroup):
waiting_text = State()
await state.set_state(EditDigestState.waiting_text)
@router.message(F.state == 'EditDigestState:waiting_text')
async def process_edit_text(message: Message, state: FSMContext):
"""Обработка отредактированного текста"""
from aiogram.fsm.state import StatesGroup, State
class EditDigestState(StatesGroup):
waiting_text = State()
new_text = message.text
data = await state.get_data()
digest_id = data.get('edit_digest_id')
try:
async with AsyncSessionLocal() as session:
service = DigestService(session)
await service.update_digest_text(digest_id, new_text)
await message.answer(f"✅ Текст дайджеста #{digest_id} обновлён!\n\nТеперь можете утвердить его в веб-панели.")
except Exception as e:
await message.answer(f"❌ Ошибка: {e}")
await state.clear()
# ============================================================================
# АВТОМАТИЧЕСКАЯ ГЕНЕРАЦИЯ (из scheduler)
# ============================================================================
async def auto_generate_digest(bot):
"""
Автоматическая генерация дайджеста (вызывается из scheduler)
"""
try:
async with AsyncSessionLocal() as session:
service = DigestService(session)
# Генерируем
digest_data = await service.generate_weekly_digest()
if not digest_data:
logger.warning("⚠️ Не удалось сгенерировать дайджест")
return
# Проверяем, нет ли уже дайджеста за эту неделю
check_stmt = select(Digest).where(
Digest.week_number == digest_data['week_number'],
Digest.year == digest_data['year']
)
result = await session.execute(check_stmt)
existing = result.scalar_one_or_none()
if existing:
logger.info(f"📰 Дайджест за неделю {digest_data['week_number']}/{digest_data['year']} уже существует")
return
# Сохраняем
digest_id = await service.save_digest(digest_data, status='pending_approval')
# Уведомляем админа
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🌐 Утвердить в веб-панели", url="http://localhost:8000/digests")],
])
await bot.send_message(
ADMIN_USER_ID,
f"📰 <b>Дайджест за неделю готов!</b>\n\n"
f"Неделя #{digest_data['week_number']}/{digest_data['year']}\n"
f"Период: {digest_data['period_start'].strftime('%d.%m')} - {digest_data['period_end'].strftime('%d.%m.%Y')}\n\n"
f"Зайдите в веб-панель для утверждения:\n"
f"http://localhost:8000/digests",
parse_mode='HTML',
reply_markup=keyboard
)
logger.info(f"✅ Дайджест #{digest_id} сгенерирован и ожидает утверждения")
except Exception as e:
logger.error(f"Ошибка авто-генерации дайджеста: {e}")
await bot.send_message(
ADMIN_USER_ID,
f"❌ Ошибка генерации дайджеста: {e}"
)