"""Планировщик рассылок. Исполняется внутри процесса бота. Время и дни недели пока заданы в коде (данные, а не магические числа); в Блоке 4 они переедут в таблицу ``schedules`` и будут редактироваться через админку. Сами тексты уже берутся из БД (``bot.message_templates``) через ``common.texts.store``. """ from __future__ import annotations import asyncio import logging from dataclasses import dataclass, field from datetime import datetime from aiogram import Bot from bot.services import users as users_service from common.config import get_settings from common.texts import format_usernames, render from common.texts.store import get_text logger = logging.getLogger(__name__) #: Размер порции упоминаний в одном сообщении. BATCH_SIZE = 10 @dataclass(frozen=True, slots=True) class ScheduledMessage: """Одна запланированная рассылка. :param weekdays: дни недели (0=Пн … 6=Вс). None — ежедневно. """ hour: int minute: int template_key: str weekdays: tuple[int, ...] | None = field(default=None) def matches(self, now: datetime) -> bool: if self.hour != now.hour or self.minute != now.minute: return False return self.weekdays is None or now.weekday() in self.weekdays SCHEDULES: tuple[ScheduledMessage, ...] = ( ScheduledMessage(16, 30, "notify.blago_30"), ScheduledMessage(16, 45, "notify.blago_45"), ScheduledMessage(16, 55, "notify.blago_55"), ScheduledMessage(17, 0, "notify.blago"), ScheduledMessage(15, 0, "notify.vklad", weekdays=(4,)), # пятница ScheduledMessage(14, 0, "notify.vklad", weekdays=(6,)), # воскресенье ) async def _send_broadcast(bot: Bot, template: str, subscribers: list[dict]) -> None: chunks = [subscribers[i : i + BATCH_SIZE] for i in range(0, len(subscribers), BATCH_SIZE)] or [[]] for chunk in chunks: text = render(template, {"usernames": format_usernames(chunk)}) await bot.send_message(chat_id=get_settings().chat_id, text=text) async def _tick(bot: Bot, now: datetime) -> None: for schedule in SCHEDULES: if schedule.matches(now): subscribers = await users_service.list_subscribers() template = await get_text(schedule.template_key) await _send_broadcast(bot, template, subscribers) async def run_notifications(bot: Bot) -> None: """Бесконечно проверять расписание раз в минуту.""" tz = get_settings().tzinfo while True: now = datetime.now(tz).replace(second=0, microsecond=0) try: await _tick(bot, now) except asyncio.CancelledError: raise except Exception: logger.exception("Ошибка планировщика рассылок") # Спать до начала следующей минуты, чтобы обрабатывать каждую минуту один раз. await asyncio.sleep(60 - datetime.now(tz).second)