"""Планировщик рассылок. Исполняется внутри процесса бота. Расписание пока задано в коде (данные, а не магические числа); в Блоке 4 переедет в таблицу ``schedules`` и будет редактироваться через админку. """ 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 import notifications as notify_texts logger = logging.getLogger(__name__) #: Размер порции упоминаний в одном сообщении. BATCH_SIZE = 10 @dataclass(frozen=True, slots=True) class ScheduledMessage: """Одна запланированная рассылка. :param weekdays: дни недели (0=Пн … 6=Вс). None — ежедневно. """ hour: int minute: int template: 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_texts.MSG_BLAGO_30), ScheduledMessage(16, 45, notify_texts.MSG_BLAGO_45), ScheduledMessage(16, 55, notify_texts.MSG_BLAGO_55), ScheduledMessage(17, 0, notify_texts.MSG_BLAGO), ScheduledMessage(15, 0, notify_texts.MSG_VKLAD, weekdays=(4,)), # пятница ScheduledMessage(14, 0, notify_texts.MSG_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: settings = get_settings() if now.minute == 0 and settings.alert_chat_id: # Пинг «жив» в чат логов (если задан). await bot.send_message(chat_id=settings.alert_chat_id, text="Я ЖЫФ!") for schedule in SCHEDULES: if schedule.matches(now): subscribers = await users_service.list_subscribers() await _send_broadcast(bot, schedule.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)