DispelEvilBot/bot/services/notifications.py

85 lines
3.2 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.

"""Планировщик рассылок.
Исполняется внутри процесса бота. Время и дни недели пока заданы в коде
(данные, а не магические числа); в Блоке 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)