diff --git a/bot/handlers/__init__.py b/bot/handlers/__init__.py index c872ad9..56adc9f 100644 --- a/bot/handlers/__init__.py +++ b/bot/handlers/__init__.py @@ -2,6 +2,7 @@ from bot.handlers.start import start, help_command from bot.handlers.reminder import reminder_conv_handler from bot.handlers.list import list_reminders from bot.handlers.callback import handle_callback +from bot.handlers.snooze import handle_snooze_input __all__ = [ "start", @@ -9,4 +10,5 @@ __all__ = [ "reminder_conv_handler", "list_reminders", "handle_callback", + "handle_snooze_input", ] diff --git a/bot/handlers/callback.py b/bot/handlers/callback.py index e07dbda..ed11cf8 100644 --- a/bot/handlers/callback.py +++ b/bot/handlers/callback.py @@ -14,8 +14,14 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> await query.answer() data = query.data - action, reminder_id_str = data.split("_", 1) - reminder_id = int(reminder_id_str) + + # Support compound actions like "cancel_snooze_123" + if data.startswith("cancel_snooze_"): + action = "cancel_snooze" + reminder_id = int(data[len("cancel_snooze_"):]) + else: + action, reminder_id_str = data.split("_", 1) + reminder_id = int(reminder_id_str) session = Session() try: @@ -31,20 +37,23 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> await query.edit_message_text(f"✅ 已完成提醒:{reminder.title}") elif action == "snooze": - # Snooze for 10 minutes - send another reminder - from bot.scheduler.executor import execute_reminder - - snooze_time = datetime.now(timezone.utc) + timedelta(minutes=10) - context.job_queue.run_once( - lambda ctx: execute_reminder(reminder_id, ctx.bot), - when=snooze_time, - ) - tz = pytz.timezone("Asia/Shanghai") - display_time = snooze_time.astimezone(tz).strftime('%H:%M') + # Ask user for snooze minutes + context.user_data["snooze_reminder_id"] = reminder_id + context.user_data["snooze_chat_id"] = query.message.chat_id await query.edit_message_text( - f"⏰ 已延期10分钟,将在 {display_time} 再次提醒。" + "⏰ 请输入延期的分钟数(例如:10):" ) + elif action == "cancel_snooze": + job_name = f"snooze_{reminder_id}" + jobs = context.job_queue.get_jobs_by_name(job_name) + if jobs: + for job in jobs: + job.schedule_removal() + await query.edit_message_text(f"❌ 已关闭延期提醒:{reminder.title}") + else: + await query.edit_message_text("延期提醒不存在或已过期。") + elif action == "pause": reminder.is_active = False session.commit() @@ -69,3 +78,4 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> await query.edit_message_text("操作失败,请稍后重试。") finally: Session.remove() + diff --git a/bot/handlers/snooze.py b/bot/handlers/snooze.py new file mode 100644 index 0000000..7bca517 --- /dev/null +++ b/bot/handlers/snooze.py @@ -0,0 +1,72 @@ +from datetime import datetime, timedelta, timezone + +import pytz +from telegram import Update +from telegram.ext import ContextTypes + +from bot.models.database import Session +from bot.models.reminder import Reminder +from bot.utils.keyboards import snooze_action_keyboard + + +async def handle_snooze_input(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Handle user input for snooze minutes.""" + if "snooze_reminder_id" not in context.user_data: + return + + reminder_id = context.user_data.pop("snooze_reminder_id") + chat_id = context.user_data.pop("snooze_chat_id", None) + + try: + minutes = int(update.message.text.strip()) + if minutes <= 0 or minutes > 1440: + await update.message.reply_text("请输入 1-1440 之间的分钟数。") + return + except ValueError: + await update.message.reply_text("请输入有效的数字。") + return + + session = Session() + try: + reminder = session.get(Reminder, reminder_id) + if reminder is None: + await update.message.reply_text("提醒不存在或已被删除。") + return + + snooze_time_utc = datetime.now(timezone.utc) + timedelta(minutes=minutes) + + # For once-type reminders, update the once_time and reschedule + if reminder.reminder_type == "once": + reminder.once_time = snooze_time_utc + reminder.is_active = True + session.commit() + + # Reschedule using APScheduler + from bot.scheduler.job_manager import add_reminder_job + add_reminder_job(reminder_id) + else: + # For recurring reminders, use temporary job_queue + from bot.scheduler.executor import execute_reminder + _rid = reminder_id + + async def snooze_callback(ctx): + await execute_reminder(_rid, ctx.bot) + + context.job_queue.run_once( + snooze_callback, + when=snooze_time_utc, + name=f"snooze_{reminder_id}", + ) + + tz = pytz.timezone("Asia/Shanghai") + display_time = snooze_time_utc.astimezone(tz).strftime("%Y-%m-%d %H:%M") + + # Only show cancel button for recurring reminders (once-type uses DB now) + keyboard = None if reminder.reminder_type == "once" else snooze_action_keyboard(reminder_id) + + await update.message.reply_text( + f"⏰ 已延期 {minutes} 分钟\n将在 {display_time} 再次提醒:{reminder.title}", + reply_markup=keyboard, + ) + finally: + Session.remove() diff --git a/bot/main.py b/bot/main.py index 1499ea6..6474f67 100644 --- a/bot/main.py +++ b/bot/main.py @@ -13,6 +13,7 @@ from telegram.ext import ( from bot.config import BOT_TOKEN, LOG_LEVEL from bot.handlers import ( handle_callback, + handle_snooze_input, help_command, list_reminders, reminder_conv_handler, @@ -39,7 +40,7 @@ def main() -> None: # Initialize scheduler init_scheduler(app.bot) - # Register handlers + # Register handlers (group=0) app.add_handler(CommandHandler("start", start)) app.add_handler(CommandHandler("help", help_command)) app.add_handler(MessageHandler(filters.Regex("^❓ 帮助$"), help_command)) @@ -51,6 +52,13 @@ def main() -> None: app.add_handler(CallbackQueryHandler(handle_callback)) + # Snooze minutes input in group=1, after ConversationHandler. + # handle_snooze_input checks user_data and returns early if not waiting for input. + app.add_handler( + MessageHandler(filters.TEXT & ~filters.COMMAND, handle_snooze_input), + group=1, + ) + # Graceful shutdown def signal_handler(sig, frame): logger.info("Shutting down...") diff --git a/bot/models/reminder.py b/bot/models/reminder.py index 2f269a2..777188a 100644 --- a/bot/models/reminder.py +++ b/bot/models/reminder.py @@ -62,7 +62,19 @@ class Reminder(Base): @classmethod def get_user_reminders(cls, session: Session, user_id: int) -> List["Reminder"]: - return session.query(cls).filter_by(user_id=user_id).order_by(cls.created_at).all() + from sqlalchemy import and_, or_ + return ( + session.query(cls) + .filter( + cls.user_id == user_id, + or_( + cls.reminder_type != "once", + and_(cls.reminder_type == "once", cls.is_active == True), + ), + ) + .order_by(cls.created_at) + .all() + ) def type_display(self) -> str: mapping = { diff --git a/bot/scheduler/job_manager.py b/bot/scheduler/job_manager.py index b84d8a6..ed80960 100644 --- a/bot/scheduler/job_manager.py +++ b/bot/scheduler/job_manager.py @@ -102,9 +102,10 @@ def _build_trigger(reminder: Reminder): run_time = reminder.once_time if run_time.tzinfo is None: run_time = pytz.utc.localize(run_time) - if run_time <= datetime.now(timezone.utc).astimezone(SHANGHAI_TZ): + # Check if already passed (compare in UTC) + if run_time <= datetime.now(timezone.utc): return None # already passed - return DateTrigger(run_date=run_time, timezone=SHANGHAI_TZ) + return DateTrigger(run_date=run_time, timezone=pytz.utc) if rtype == "daily": if not reminder.daily_time: diff --git a/bot/utils/keyboards.py b/bot/utils/keyboards.py index 29332a2..7837136 100644 --- a/bot/utils/keyboards.py +++ b/bot/utils/keyboards.py @@ -45,7 +45,7 @@ def reminder_action_keyboard(reminder_id: int) -> InlineKeyboardMarkup: [ InlineKeyboardButton("✅ 完成", callback_data=f"done_{reminder_id}"), InlineKeyboardButton( - "⏰ 延期10分钟", callback_data=f"snooze_{reminder_id}" + "⏰ 延期", callback_data=f"snooze_{reminder_id}" ), ], [ @@ -56,6 +56,18 @@ def reminder_action_keyboard(reminder_id: int) -> InlineKeyboardMarkup: ) +def snooze_action_keyboard(reminder_id: int) -> InlineKeyboardMarkup: + return InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "❌ 关闭延期提醒", callback_data=f"cancel_snooze_{reminder_id}" + ) + ] + ] + ) + + def list_item_keyboard(reminder_id: int, is_active: bool) -> InlineKeyboardMarkup: toggle_label = "⏸ 暂停" if is_active else "▶️ 启用" toggle_action = "pause" if is_active else "resume" diff --git a/docker-compose.yml b/docker-compose.yml index a7f7b63..56fa5d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,4 +9,5 @@ services: - ./data:/app/data - ./logs:/app/logs environment: + - TZ=Asia/Shanghai