From 2670ca96c770304c25e4888ff1ac7f7765625f01 Mon Sep 17 00:00:00 2001 From: Xiaolan Bot Date: Sun, 22 Feb 2026 23:56:36 +0800 Subject: [PATCH] fix(ui): migrate parse_mode from MarkdownV2 to HTML to prevent parsing crashes --- SubMind.py | 102 ++++++++++++++++++++++++++--------------------------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/SubMind.py b/SubMind.py index 7868dab..68d36b6 100644 --- a/SubMind.py +++ b/SubMind.py @@ -1,5 +1,6 @@ import sqlite3 import os +import html import requests import datetime import dateparser @@ -18,7 +19,7 @@ from telegram.ext import ( CallbackContext, CallbackQueryHandler, ConversationHandler ) from telegram.error import TelegramError -from telegram.helpers import escape_markdown +from telegram.helpers import escape_html # --- 加载 .env 和设置 --- load_dotenv() @@ -360,7 +361,7 @@ async def check_and_send_reminders(context: CallbackContext): due_date = datetime.datetime.strptime(sub['next_due'], '%Y-%m-%d').date() user_id = sub['user_id'] renewal_type = sub['renewal_type'] - safe_sub_name = escape_markdown(sub['name'], version=2) + safe_sub_name = escape_html(sub['name']) message = None keyboard = None @@ -382,14 +383,14 @@ async def check_and_send_reminders(context: CallbackContext): reminder_date = due_date - datetime.timedelta(days=sub['reminder_days']) if reminder_date == today: days_left = (due_date - today).days - days_text = f"*{days_left}天后*" if days_left > 0 else "*今天*" + days_text = f"{days_left}天后" if days_left > 0 else "今天" message = f"🔔 *订阅即将到期提醒*\n\n您的手动续费订阅 `{safe_sub_name}` 将在 {days_text} 到期。" if message: await context.bot.send_message( chat_id=user_id, text=message, - parse_mode='MarkdownV2', + parse_mode='HTML', reply_markup=keyboard ) # 记录今天已发送提醒 @@ -408,13 +409,13 @@ async def start(update: Update, context: CallbackContext): cursor = conn.cursor() cursor.execute('INSERT OR IGNORE INTO users (user_id) VALUES (?)', (user_id,)) conn.commit() - await update.message.reply_text(f'欢迎使用 {escape_markdown(PROJECT_NAME, version=2)}!\n您的私人订阅智能管家。', - parse_mode='MarkdownV2') + await update.message.reply_text(f'欢迎使用 {escape_html(PROJECT_NAME)}!\n您的私人订阅智能管家。', + parse_mode='HTML') async def help_command(update: Update, context: CallbackContext): help_text = fr""" -*{escape_markdown(PROJECT_NAME, version=2)} 命令列表* +*{escape_html(PROJECT_NAME)} 命令列表* *🌟 核心功能* /add\_sub \- 引导您添加一个新的订阅 /list\_subs \- 列出您的所有订阅 @@ -427,7 +428,7 @@ async def help_command(update: Update, context: CallbackContext): /set\_currency \`\` \- 设置您的主要货币 /cancel \- 在任何流程中取消当前操作 """ - await update.message.reply_text(help_text, parse_mode='MarkdownV2') + await update.message.reply_text(help_text, parse_mode='HTML') def make_autopct(values, currency_code): @@ -722,8 +723,8 @@ async def import_upload_received(update: Update, context: CallbackContext): # --- Add Subscription Conversation --- async def add_sub_start(update: Update, context: CallbackContext): context.user_data['new_sub_data'] = {} - await update.message.reply_text("好的,我们来添加一个新订阅。\n\n第一步:请输入订阅的 *名称*", - parse_mode='MarkdownV2') + await update.message.reply_text("好的,我们来添加一个新订阅。\n\n第一步:请输入订阅的 名称", + parse_mode='HTML') return ADD_NAME @@ -742,7 +743,7 @@ async def add_name_received(update: Update, context: CallbackContext): await update.message.reply_text(f"订阅名称过长,请控制在 {MAX_NAME_LEN} 个字符以内。") return ADD_NAME sub_data['name'] = name - await update.message.reply_text("第二步:请输入订阅 *费用*", parse_mode='MarkdownV2') + await update.message.reply_text("第二步:请输入订阅 费用", parse_mode='HTML') return ADD_COST @@ -761,7 +762,7 @@ async def add_cost_received(update: Update, context: CallbackContext): except (ValueError, TypeError): await update.message.reply_text("费用必须是有效的非负数字。") return ADD_COST - await update.message.reply_text("第三步:请输入 *货币* 代码(例如 USD, CNY)", parse_mode='MarkdownV2') + await update.message.reply_text("第三步:请输入 货币 代码(例如 USD, CNY)", parse_mode='HTML') return ADD_CURRENCY @@ -777,7 +778,7 @@ async def add_currency_received(update: Update, context: CallbackContext): await update.message.reply_text("请输入有效的三字母货币代码(如 USD, CNY)。") return ADD_CURRENCY sub_data['currency'] = currency - await update.message.reply_text("第四步:请为订阅指定一个 *类别*", parse_mode='MarkdownV2') + await update.message.reply_text("第四步:请为订阅指定一个 类别", parse_mode='HTML') return ADD_CATEGORY @@ -801,7 +802,7 @@ async def add_category_received(update: Update, context: CallbackContext): cursor.execute("INSERT OR IGNORE INTO categories (user_id, name) VALUES (?, ?)", (user_id, category_name)) conn.commit() await update.message.reply_text("第五步:请输入 *下一次付款日期*(例如 2025\\-10\\-01 或 10月1日)", - parse_mode='MarkdownV2') + parse_mode='HTML') return ADD_NEXT_DUE @@ -823,8 +824,8 @@ async def add_next_due_received(update: Update, context: CallbackContext): [InlineKeyboardButton("月", callback_data='freq_unit_month'), InlineKeyboardButton("年", callback_data='freq_unit_year')] ] - await update.message.reply_text("第六步:请选择付款周期的*单位*", reply_markup=InlineKeyboardMarkup(keyboard), - parse_mode='MarkdownV2') + await update.message.reply_text("第六步:请选择付款周期的单位", reply_markup=InlineKeyboardMarkup(keyboard), + parse_mode='HTML') return ADD_FREQ_UNIT @@ -841,7 +842,7 @@ async def add_freq_unit_received(update: Update, context: CallbackContext): await query.edit_message_text("错误:无效的周期单位,请重试。") return ConversationHandler.END sub_data['unit'] = unit - await query.edit_message_text("第七步:请输入周期的*数量*(例如:每3个月,输入 3)", parse_mode='Markdown') + await query.edit_message_text("第七步:请输入周期的数量(例如:每3个月,输入 3)", parse_mode='Markdown') return ADD_FREQ_VALUE @@ -864,8 +865,8 @@ async def add_freq_value_received(update: Update, context: CallbackContext): [InlineKeyboardButton("自动续费", callback_data='renewal_auto'), InlineKeyboardButton("手动续费", callback_data='renewal_manual')] ] - await update.message.reply_text("第八步:请选择 *续费方式*", reply_markup=InlineKeyboardMarkup(keyboard), - parse_mode='MarkdownV2') + await update.message.reply_text("第八步:请选择 续费方式", reply_markup=InlineKeyboardMarkup(keyboard), + parse_mode='HTML') return ADD_RENEWAL_TYPE @@ -900,8 +901,8 @@ async def add_notes_received(update: Update, context: CallbackContext): return ADD_NOTES sub_data['notes'] = note if note else None save_subscription(update.effective_user.id, sub_data) - await update.message.reply_text(text=f"✅ 订阅 '{escape_markdown(sub_data.get('name', ''), version=2)}' 已添加!", - parse_mode='MarkdownV2') + await update.message.reply_text(text=f"✅ 订阅 '{escape_html(sub_data.get('name', ''))}' 已添加!", + parse_mode='HTML') _clear_action_state(context, ['new_sub_data']) return ConversationHandler.END @@ -916,8 +917,8 @@ async def skip_notes(update: Update, context: CallbackContext): sub_data['notes'] = None save_subscription(update.effective_user.id, sub_data) - await update.message.reply_text(text=f"✅ 订阅 '{escape_markdown(sub_data.get('name', ''), version=2)}' 已添加!", - parse_mode='MarkdownV2') + await update.message.reply_text(text=f"✅ 订阅 '{escape_html(sub_data.get('name', ''))}' 已添加!", + parse_mode='HTML') _clear_action_state(context, ['new_sub_data']) return ConversationHandler.END @@ -989,11 +990,8 @@ async def show_subscription_view(update: Update, context: CallbackContext, sub_i freq_text = format_frequency(sub['frequency_unit'], sub['frequency_value']) main_currency = get_user_main_currency(user_id) converted_cost = convert_currency(cost, currency, main_currency) - safe_name, safe_category, safe_freq = escape_markdown(name, version=2), escape_markdown(category, - version=2), escape_markdown( - freq_text, version=2) - cost_str, converted_cost_str = escape_markdown(f"{cost:.2f}", version=2), escape_markdown(f"{converted_cost:.2f}", - version=2) + safe_name, safe_category, safe_freq = escape_html(name), escape_html(category), escape_html(freq_text) + cost_str, converted_cost_str = escape_html(f"{cost:.2f}"), escape_html(f"{converted_cost:.2f}") renewal_text = "手动续费" if renewal_type == 'manual' else "自动续费" reminder_status = "开启" if reminders_enabled else "关闭" text = (f"*订阅详情: {safe_name}*\n\n" @@ -1003,7 +1001,7 @@ async def show_subscription_view(update: Update, context: CallbackContext, sub_i f"\\- *续费方式*: `{renewal_text}`\n" f"\\- *提醒状态*: `{reminder_status}`") if notes: - text += f"\n\\- *备注*: {escape_markdown(notes, version=2)}" + text += f"\n\\- *备注*: {escape_html(notes)}" keyboard_buttons = [ [InlineKeyboardButton("✏️ 编辑", callback_data=f'edit_{sub_id}'), InlineKeyboardButton("🗑️ 删除", callback_data=f'delete_{sub_id}')], @@ -1024,10 +1022,10 @@ async def show_subscription_view(update: Update, context: CallbackContext, sub_i logger.debug(f"Generated buttons for sub_id {sub_id}: edit_{sub_id}, remind_{sub_id}") if update.callback_query: await update.callback_query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard_buttons), - parse_mode='MarkdownV2') + parse_mode='HTML') elif update.effective_message: await update.effective_message.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard_buttons), - parse_mode='MarkdownV2') + parse_mode='HTML') async def button_callback_handler(update: Update, context: CallbackContext): @@ -1056,11 +1054,11 @@ async def button_callback_handler(update: Update, context: CallbackContext): context.user_data['list_subs_in_category'] = category context.user_data['list_subs_in_category_id'] = category_id keyboard = await get_subs_list_keyboard(user_id, category_filter=category) - msg_text = f"分类“{escape_markdown(category, version=2)}”下的订阅:" + msg_text = f"分类 {escape_html(category)} 下的订阅:" if not keyboard: - msg_text = f"分类“{escape_markdown(category, version=2)}”下没有订阅。" + msg_text = f"分类 {escape_html(category)} 下没有订阅。" keyboard = InlineKeyboardMarkup([[InlineKeyboardButton("« 返回分类列表", callback_data='list_categories')]]) - await query.edit_message_text(msg_text, reply_markup=keyboard, parse_mode='MarkdownV2') + await query.edit_message_text(msg_text, reply_markup=keyboard, parse_mode='HTML') return if data == 'list_categories': context.user_data.pop('list_subs_in_category', None) @@ -1130,10 +1128,10 @@ async def button_callback_handler(update: Update, context: CallbackContext): (new_date_str, sub_id, user_id) ) conn.commit() - safe_sub_name = escape_markdown(sub['name'], version=2) + safe_sub_name = escape_html(sub['name']) await query.edit_message_text( - text=f"✅ *续费成功*\n\n您的订阅 `{safe_sub_name}` 新的到期日为: `{new_date_str}`", - parse_mode='MarkdownV2', + text=f"✅ 续费成功\n\n您的订阅 {safe_sub_name} 新的到期日为: {new_date_str}", + parse_mode='HTML', reply_markup=None ) else: @@ -1141,7 +1139,7 @@ async def button_callback_handler(update: Update, context: CallbackContext): else: await query.answer("续费失败:此订阅可能已被删除或无权限。", show_alert=True) await query.edit_message_text(text=query.message.text + "\n\n*(错误:此订阅不存在或无权限)*", - parse_mode='MarkdownV2', reply_markup=None) + parse_mode='HTML', reply_markup=None) elif action == 'delete': with get_db_connection() as conn: @@ -1168,12 +1166,12 @@ async def button_callback_handler(update: Update, context: CallbackContext): if 'list_subs_in_category' in context.user_data: category = context.user_data['list_subs_in_category'] keyboard = await get_subs_list_keyboard(user_id, category_filter=category) - msg_text = f"分类“{escape_markdown(category, version=2)}”下的订阅:" + msg_text = f"分类 {escape_html(category)} 下的订阅:" if not keyboard: - msg_text = f"分类“{escape_markdown(category, version=2)}”下没有订阅。" + msg_text = f"分类 {escape_html(category)} 下没有订阅。" keyboard = InlineKeyboardMarkup( [[InlineKeyboardButton("« 返回分类列表", callback_data='list_categories')]]) - await query.edit_message_text(msg_text, reply_markup=keyboard, parse_mode='MarkdownV2') + await query.edit_message_text(msg_text, reply_markup=keyboard, parse_mode='HTML') else: keyboard = await get_subs_list_keyboard(user_id) if not keyboard: @@ -1249,16 +1247,16 @@ async def edit_field_selected(update: Update, context: CallbackContext): [InlineKeyboardButton("月", callback_data='freq_unit_month'), InlineKeyboardButton("年", callback_data='freq_unit_year')] ] - await query.edit_message_text("请选择新的周期*单位*", reply_markup=InlineKeyboardMarkup(keyboard), - parse_mode='MarkdownV2') + await query.edit_message_text("请选择新的周期单位", reply_markup=InlineKeyboardMarkup(keyboard), + parse_mode='HTML') return EDIT_FREQ_UNIT else: field_map = {'name': '名称', 'cost': '费用', 'currency': '货币', 'category': '类别', 'next_due': '下次付款日', 'notes': '备注'} - prompt = f"好的,请输入新的 *{field_map.get(field_to_edit, field_to_edit)}* 值:" + prompt = f"好的,请输入新的 {field_map.get(field_to_edit, field_to_edit)} 值:" if field_to_edit == 'notes': prompt += "\n(如需清空备注,请输入 /empty )" - await query.edit_message_text(prompt, parse_mode='MarkdownV2') + await query.edit_message_text(prompt, parse_mode='HTML') return EDIT_GET_NEW_VALUE @@ -1270,7 +1268,7 @@ async def edit_freq_unit_received(update: Update, context: CallbackContext): await query.edit_message_text("错误:无效的周期单位,请重试。") return ConversationHandler.END context.user_data['new_freq_unit'] = unit - await query.edit_message_text("好的,现在请输入新的周期*数量*。", parse_mode='MarkdownV2') + await query.edit_message_text("好的,现在请输入新的周期数量。", parse_mode='HTML') return EDIT_FREQ_VALUE @@ -1445,13 +1443,13 @@ async def _display_reminder_settings(query: CallbackQuery, context: CallbackCont [InlineKeyboardButton(enabled_text, callback_data='remindaction_toggle_enabled')], [InlineKeyboardButton(due_date_text, callback_data='remindaction_toggle_due_date')] ] - safe_name = escape_markdown(sub['name'], version=2) - current_status = f"*🔔 提醒设置: {safe_name}*\n\n" + safe_name = escape_html(sub['name']) + current_status = f"🔔 提醒设置: {safe_name}\n\n" if sub['renewal_type'] == 'manual': current_status += f"当前提前提醒: *{sub['reminder_days']}天*\n" keyboard.append([InlineKeyboardButton("⚙️ 更改提前天数", callback_data='remindaction_ask_days')]) keyboard.append([InlineKeyboardButton("« 返回详情", callback_data=f'view_{sub_id}')]) - await query.edit_message_text(current_status, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode='MarkdownV2') + await query.edit_message_text(current_status, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode='HTML') async def remind_settings_start(update: Update, context: CallbackContext): @@ -1553,7 +1551,7 @@ async def remind_days_received(update: Update, context: CallbackContext): async def set_currency(update: Update, context: CallbackContext): user_id, args = update.effective_user.id, context.args if len(args) != 1: - await update.message.reply_text("用法: /set_currency ``(例如 /set_currency USD)", parse_mode='MarkdownV2') + await update.message.reply_text("用法: /set_currency 代码(例如 /set_currency USD)", parse_mode='HTML') return new_currency = args[0].upper() if len(new_currency) != 3 or not new_currency.isalpha(): @@ -1567,8 +1565,8 @@ async def set_currency(update: Update, context: CallbackContext): ON CONFLICT(user_id) DO UPDATE SET main_currency = excluded.main_currency """, (user_id, new_currency)) conn.commit() - await update.message.reply_text(f"您的主货币已设为 {escape_markdown(new_currency, version=2)}。", - parse_mode='MarkdownV2') + await update.message.reply_text(f"您的主货币已设为 {escape_html(new_currency)}。", + parse_mode='HTML') return ConversationHandler.END