ReminderBot/bot/scheduler/executor.py
leo f453a7917e Initial commit: add reminderBot project structure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 11:40:58 +08:00

101 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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