reminder-bot/bot/core/scheduler.py

153 lines
4.4 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
"""
from bot.db.base import async_session_maker
from bot.db.operations import update_reminder
if not async_session_maker:
logger.error("Session maker not initialized")
return
try:
time_service = get_time_service()
current_time = time_service.get_now()
# Get due reminders from database using proper async session
async with async_session_maker() as 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)
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)
# Commit all updates
await session.commit()
# 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