status bar in sender service
This commit is contained in:
@@ -16,6 +16,22 @@ from .sender_states import AdminSender
|
||||
from .sender_utils import get_recipients, parse_message_buttons
|
||||
|
||||
|
||||
def _broadcast_progress_text(completed: int, total: int, sent: int, failed: int) -> str:
|
||||
"""Формирует текст статус-бара рассылки."""
|
||||
if total <= 0:
|
||||
pct = 0
|
||||
bar_filled = 0
|
||||
else:
|
||||
pct = min(100, int(100 * completed / total))
|
||||
bar_filled = min(10, int(10 * completed / total))
|
||||
bar = "█" * bar_filled + "░" * (10 - bar_filled)
|
||||
return (
|
||||
f"📤 <b>Рассылка...</b>\n\n"
|
||||
f"[{bar}] <b>{pct}%</b> ({completed}/{total})\n"
|
||||
f"✅ {sent} ❌ {failed}"
|
||||
)
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@@ -170,16 +186,39 @@ async def handle_broadcast_confirm(callback_query: CallbackQuery, state: FSMCont
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
await callback_query.message.edit_text(f"📤 <b>Рассылка начата!</b>\n👥 Количество получателей: {total_users}")
|
||||
status_message = callback_query.message
|
||||
total_users_for_bar = len(tg_ids)
|
||||
await status_message.edit_text(
|
||||
_broadcast_progress_text(0, total_users_for_bar, 0, 0),
|
||||
)
|
||||
|
||||
messages = []
|
||||
for tg_id in tg_ids:
|
||||
message_data = {"tg_id": tg_id, "text": text_message, "photo": photo, "keyboard": keyboard}
|
||||
messages.append(message_data)
|
||||
|
||||
broadcast_service = BroadcastService(bot=callback_query.bot, session=session, messages_per_second=35)
|
||||
bot = callback_query.bot
|
||||
|
||||
stats = await broadcast_service.broadcast(messages, workers=5)
|
||||
async def on_progress(completed: int, total: int, sent: int, failed: int) -> None:
|
||||
text = _broadcast_progress_text(completed, total, sent, failed)
|
||||
try:
|
||||
await bot.edit_message_text(
|
||||
chat_id=status_message.chat.id,
|
||||
message_id=status_message.message_id,
|
||||
text=text,
|
||||
)
|
||||
except TelegramBadRequest as e:
|
||||
if "message is not modified" not in str(e).lower():
|
||||
logger.debug(f"[Sender] Обновление прогресса: {e}")
|
||||
|
||||
broadcast_service = BroadcastService(bot=bot, session=session, messages_per_second=35)
|
||||
|
||||
stats = await broadcast_service.broadcast(
|
||||
messages,
|
||||
workers=5,
|
||||
on_progress=on_progress,
|
||||
progress_interval=2.0,
|
||||
)
|
||||
|
||||
duration_minutes = int(stats["total_duration"] // 60)
|
||||
duration_seconds = int(stats["total_duration"] % 60)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from collections import deque
|
||||
from typing import Any
|
||||
|
||||
@@ -183,7 +184,32 @@ class BroadcastService:
|
||||
if self._session is not None:
|
||||
await self._session.rollback()
|
||||
|
||||
async def broadcast(self, messages: list[dict], workers: int = 20) -> dict:
|
||||
async def _progress_loop(
|
||||
self,
|
||||
total: int,
|
||||
on_progress: Callable[[int, int, int, int], Awaitable[None]],
|
||||
interval: float,
|
||||
) -> None:
|
||||
"""Периодически вызывает on_progress(completed, total, sent, failed)."""
|
||||
while self.is_running:
|
||||
await asyncio.sleep(interval)
|
||||
if not self.is_running:
|
||||
break
|
||||
completed = len(self.results)
|
||||
sent = self.total_sent
|
||||
failed = completed - sent
|
||||
try:
|
||||
await on_progress(completed, total, sent, failed)
|
||||
except Exception as e:
|
||||
logger.debug(f"[Broadcast] Ошибка обновления прогресса: {e}")
|
||||
|
||||
async def broadcast(
|
||||
self,
|
||||
messages: list[dict],
|
||||
workers: int = 20,
|
||||
on_progress: Callable[[int, int, int, int], Awaitable[None]] | None = None,
|
||||
progress_interval: float = 2.0,
|
||||
) -> dict:
|
||||
self.is_running = True
|
||||
self.start_time = time.time()
|
||||
self.results = []
|
||||
@@ -201,6 +227,13 @@ class BroadcastService:
|
||||
|
||||
logger.info(f"📤 Начата рассылка на {len(messages)} пользователей с {workers} воркерами")
|
||||
|
||||
total = len(messages)
|
||||
progress_task = None
|
||||
if on_progress and total > 0:
|
||||
progress_task = asyncio.create_task(
|
||||
self._progress_loop(total, on_progress, progress_interval),
|
||||
)
|
||||
|
||||
worker_tasks = [asyncio.create_task(self._worker()) for _ in range(workers)]
|
||||
|
||||
delayed_task = asyncio.create_task(self._process_delayed_messages())
|
||||
@@ -213,6 +246,23 @@ class BroadcastService:
|
||||
|
||||
self.is_running = False
|
||||
|
||||
if progress_task is not None:
|
||||
progress_task.cancel()
|
||||
try:
|
||||
await progress_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
completed = len(self.results)
|
||||
try:
|
||||
await on_progress(
|
||||
completed,
|
||||
total,
|
||||
self.total_sent,
|
||||
completed - self.total_sent,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"[Broadcast] Финальное обновление прогресса: {e}")
|
||||
|
||||
for task in worker_tasks:
|
||||
task.cancel()
|
||||
delayed_task.cancel()
|
||||
|
||||
@@ -17,7 +17,7 @@ from aiogram.exceptions import (
|
||||
from aiogram.types import BufferedInputFile, InlineKeyboardMarkup
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from database import create_blocked_user
|
||||
from database import async_session_maker, create_blocked_user
|
||||
from handlers.tariffs.tariff_display import get_key_tariff_display
|
||||
from handlers.utils import format_hours, format_minutes, get_russian_month
|
||||
from logger import logger
|
||||
@@ -147,7 +147,7 @@ class FastNotificationSender:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def _save_blocked_users(self):
|
||||
if not self.blocked_users or not self.session:
|
||||
if not self.blocked_users:
|
||||
return
|
||||
try:
|
||||
from sqlalchemy.dialects.postgresql import insert
|
||||
@@ -155,12 +155,12 @@ class FastNotificationSender:
|
||||
|
||||
values = [{"tg_id": tg_id} for tg_id in self.blocked_users]
|
||||
stmt = insert(BlockedUser).values(values).on_conflict_do_nothing(index_elements=[BlockedUser.tg_id])
|
||||
await self.session.execute(stmt)
|
||||
await self.session.commit()
|
||||
async with async_session_maker() as session:
|
||||
await session.execute(stmt)
|
||||
await session.commit()
|
||||
logger.info(f"📝 Добавлено {len(self.blocked_users)} пользователей в blocked_users")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при сохранении заблокированных пользователей: {e}")
|
||||
await self.session.rollback()
|
||||
|
||||
async def send_all(self, messages: list[dict], workers: int = 15) -> list[bool]:
|
||||
if not messages:
|
||||
|
||||
Reference in New Issue
Block a user