- Replace IntervalTrigger with OrTrigger of CronTriggers for interval reminders, ensuring triggers align to window start each day without drift - Refactor executor to extract _send_reminder_message helper, removing the runtime window check (now handled at scheduling layer) - Change "done" button to only acknowledge the notification without deactivating the reminder Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
91 lines
2.7 KiB
Python
91 lines
2.7 KiB
Python
from __future__ import annotations
|
||
|
||
import logging
|
||
from datetime import datetime, 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
|
||
|
||
# Send the reminder
|
||
await _send_reminder_message(reminder, bot, session, now_utc, now_local)
|
||
|
||
except Exception:
|
||
logger.exception("Error executing reminder %d", reminder_id)
|
||
session.rollback()
|
||
finally:
|
||
Session.remove()
|
||
|
||
|
||
async def _send_reminder_message(
|
||
reminder: Reminder, bot: Bot, session, now_utc: datetime, now_local: datetime
|
||
) -> None:
|
||
"""Send a single reminder message."""
|
||
# Build message
|
||
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)
|
||
|
||
|
||
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
|