from __future__ import annotations import logging from datetime import datetime, timedelta, timezone import pytz from telegram import Bot from bot.models.database import Session from bot.models.reminder import Reminder, ReminderLog from bot.utils.holiday import is_holiday from bot.utils.keyboards import reminder_action_keyboard logger = logging.getLogger(__name__) async def execute_reminder(reminder_id: int, bot: Bot) -> None: """Send a reminder message. Called by APScheduler.""" session = Session() try: reminder = session.get(Reminder, reminder_id) if reminder is None or not reminder.is_active: return now_utc = datetime.now(timezone.utc) tz = pytz.timezone("Asia/Shanghai") now_local = now_utc.astimezone(tz) # Skip on holidays if requested if reminder.skip_holidays and is_holiday(now_local.date()): logger.info("Skipping reminder %d on holiday", reminder_id) return # For interval reminders, check time window if reminder.reminder_type == "interval": if not _in_window(now_local, reminder.interval_start_time, reminder.interval_end_time): logger.debug("Reminder %d outside time window, skipping", reminder_id) return # Build message user = reminder.logs # just to load relationship lazily – not needed here text = _build_message(reminder) keyboard = reminder_action_keyboard(reminder.id) # Fetch the telegram_id from users table from bot.models.user import User # avoid circular import at module level user_obj = session.get(User, reminder.user_id) if user_obj is None: return await bot.send_message( chat_id=user_obj.telegram_id, text=text, parse_mode="Markdown", reply_markup=keyboard, ) # Update reminder timestamps reminder.last_sent_at = now_utc log = ReminderLog(reminder_id=reminder.id, sent_at=now_utc, status="sent") session.add(log) # Deactivate one-time reminders after firing if reminder.reminder_type == "once": reminder.is_active = False session.commit() logger.info("Sent reminder %d to user %d", reminder_id, user_obj.telegram_id) except Exception: logger.exception("Error executing reminder %d", reminder_id) session.rollback() finally: Session.remove() def _in_window(now_local: datetime, start: str | None, end: str | None) -> bool: if not start or not end: return True sh, sm = map(int, start.split(":")) eh, em = map(int, end.split(":")) current_minutes = now_local.hour * 60 + now_local.minute start_minutes = sh * 60 + sm end_minutes = eh * 60 + em return start_minutes <= current_minutes <= end_minutes def _build_message(reminder: Reminder) -> str: lines = [f"⏰ *{_escape(reminder.title)}*"] if reminder.description: lines.append(f"\n{_escape(reminder.description)}") lines.append(f"\n_类型:{reminder.type_display()} | {reminder.schedule_summary()}_") return "\n".join(lines) def _escape(text: str) -> str: """Escape Markdown v1 special chars.""" for ch in ["_", "*", "`", "["]: text = text.replace(ch, f"\\{ch}") return text