reminder-bot/bot/core/scheduler.py

147 lines
4.3 KiB
Python

"""Scheduler for sending reminder notifications."""
import asyncio
from datetime import datetime
from typing import Optional
from aiogram import Bot
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.interval import IntervalTrigger
from bot.db.base import get_session
from bot.db.operations import get_due_reminders
from bot.keyboards.reminders import get_reminder_notification_keyboard
from bot.services.time_service import get_time_service
from bot.logging_config import get_logger
logger = get_logger(__name__)
# Global scheduler instance
_scheduler: Optional[AsyncIOScheduler] = None
async def send_reminder_notification(bot: Bot, user_tg_id: int, reminder_id: int, text: str) -> None:
"""
Send reminder notification to user.
Args:
bot: Bot instance
user_tg_id: Telegram user ID
reminder_id: Reminder ID
text: Reminder text
"""
try:
await bot.send_message(
chat_id=user_tg_id,
text=f"⏰ <b>Напоминание:</b>\n\n{text}",
reply_markup=get_reminder_notification_keyboard(reminder_id),
parse_mode="HTML",
)
logger.info(f"Sent reminder {reminder_id} to user {user_tg_id}")
except Exception as e:
logger.error(f"Failed to send reminder {reminder_id} to user {user_tg_id}: {e}")
async def check_and_send_reminders(bot: Bot) -> None:
"""
Check for due reminders and send notifications.
Args:
bot: Bot instance
"""
try:
time_service = get_time_service()
current_time = time_service.get_now()
# Get due reminders from database
async for session in get_session():
due_reminders = await get_due_reminders(session, current_time)
if not due_reminders:
logger.debug("No due reminders found")
return
logger.info(f"Found {len(due_reminders)} due reminders")
# Send notifications
for reminder in due_reminders:
await send_reminder_notification(
bot=bot,
user_tg_id=reminder.user.tg_user_id,
reminder_id=reminder.id,
text=reminder.text,
)
# Update next_run_at to prevent sending again
# (it will be properly updated when user clicks "Done" or by periodic update)
from bot.db.operations import update_reminder
from datetime import timedelta
# Temporarily set next_run_at to current + interval to avoid duplicate sends
temp_next_run = time_service.calculate_next_occurrence(
current_run=reminder.next_run_at,
days_interval=reminder.days_interval,
)
await update_reminder(session, reminder.id, next_run_at=temp_next_run)
# Small delay to avoid rate limits
await asyncio.sleep(0.5)
except Exception as e:
logger.error(f"Error in check_and_send_reminders: {e}", exc_info=True)
def create_scheduler(bot: Bot) -> AsyncIOScheduler:
"""
Create and configure scheduler.
Args:
bot: Bot instance
Returns:
Configured AsyncIOScheduler instance
"""
global _scheduler
scheduler = AsyncIOScheduler(timezone="UTC")
# Add job to check reminders every minute
scheduler.add_job(
check_and_send_reminders,
trigger=IntervalTrigger(minutes=1),
args=[bot],
id="check_reminders",
name="Check and send due reminders",
replace_existing=True,
)
_scheduler = scheduler
logger.info("Scheduler created and configured")
return scheduler
def start_scheduler() -> None:
"""Start the scheduler."""
if _scheduler is None:
raise RuntimeError("Scheduler not initialized. Call create_scheduler() first.")
_scheduler.start()
logger.info("Scheduler started")
def stop_scheduler() -> None:
"""Stop the scheduler."""
if _scheduler is not None:
_scheduler.shutdown()
logger.info("Scheduler stopped")
def get_scheduler() -> Optional[AsyncIOScheduler]:
"""
Get global scheduler instance.
Returns:
Scheduler instance or None
"""
return _scheduler