import asyncio import logging import random from datetime import datetime, timedelta from zoneinfo import ZoneInfo from aiogram import Bot QUESTIONS = ( "Как дела?", "Что нового?", "Что сейчас происходит?", "Чем занимаешься?", "Есть что записать?", "Как проходит день?", ) class ReminderState: def __init__(self) -> None: self.last_manual_entry_at: datetime | None = None self.changed = asyncio.Event() def mark_manual_entry(self, timezone: ZoneInfo) -> None: self.last_manual_entry_at = datetime.now(timezone) self.changed.set() def _is_allowed_time(now: datetime, start_hour: int, end_hour: int) -> bool: return start_hour <= now.hour < end_hour def _next_window_start(now: datetime, start_hour: int) -> datetime: if now.hour < start_hour: return now.replace(hour=start_hour, minute=0, second=0, microsecond=0) tomorrow = now + timedelta(days=1) return tomorrow.replace(hour=start_hour, minute=0, second=0, microsecond=0) def _move_to_allowed_time(now: datetime, start_hour: int, end_hour: int) -> datetime: if _is_allowed_time(now, start_hour, end_hour): return now return _next_window_start(now, start_hour) def _next_reminder_at( now: datetime, state: ReminderState, min_seconds: int, max_seconds: int, start_hour: int, end_hour: int, ) -> datetime: if not _is_allowed_time(now, start_hour, end_hour): candidate = _move_to_allowed_time(now, start_hour, end_hour) else: candidate = now + timedelta(seconds=random.randint(min_seconds, max_seconds)) if state.last_manual_entry_at: candidate = max(candidate, state.last_manual_entry_at + timedelta(hours=1)) return _move_to_allowed_time(candidate, start_hour, end_hour) async def send_reminders( bot: Bot, user_ids: set[int], min_seconds: int, max_seconds: int, start_hour: int, end_hour: int, timezone: ZoneInfo, state: ReminderState, ) -> None: while True: now = datetime.now(timezone) next_reminder_at = _next_reminder_at( now=now, state=state, min_seconds=min_seconds, max_seconds=max_seconds, start_hour=start_hour, end_hour=end_hour, ) sleep_seconds = max(0, (next_reminder_at - now).total_seconds()) try: await asyncio.wait_for(state.changed.wait(), timeout=sleep_seconds) except TimeoutError: pass else: state.changed.clear() continue question = random.choice(QUESTIONS) for user_id in user_ids: try: await bot.send_message(user_id, question) except Exception: logging.exception("Failed to send reminder to user_id=%s", user_id)