fix(ui): migrate parse_mode from MarkdownV2 to HTML to prevent parsing crashes

This commit is contained in:
Xiaolan Bot
2026-02-22 23:56:36 +08:00
parent 3711dd362b
commit 2670ca96c7

View File

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