103 lines
2.9 KiB
Python
103 lines
2.9 KiB
Python
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)
|