101 lines
3.3 KiB
Python
101 lines
3.3 KiB
Python
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
|