status bar in sender service

This commit is contained in:
Vladless
2026-02-24 16:01:16 +03:00
parent 51ad7cb0cc
commit fc0ae90f33
3 changed files with 98 additions and 9 deletions
+42 -3
View File
@@ -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)
+51 -1
View File
@@ -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()
+5 -5
View File
@@ -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: