fix(bot): render Review screen as plain text to avoid Markdown parsing errors
The reminder confirm screen was failing with 'can't parse entities' (400) because the body string included `[media...]` which Telegram's legacy Markdown mode tries to interpret as a link `[text](url)` and rejects when the closing `(url)` isn't present. Same risk for any user-typed body containing `*`, `_`, backticks, or `[`. Two fixes: - Add optional parseMode field to MenuView; showMenu honors it - reminderConfirmMenu and reminderDetailMenu render as plain text (parseMode: undefined) since both include user-supplied content - Replace `[media...]` brackets with `(media...)` parens in the wizard body preview so the placeholder itself can't trigger link parsing
This commit is contained in:
parent
a5bbf3a25d
commit
6a221fe043
@ -52,16 +52,19 @@ async function findOperator(ctx: Context) {
|
||||
// reply if the previous message can't be edited (e.g. a photo message -- Telegram
|
||||
// won't let us turn it back into a text message).
|
||||
async function showMenu(ctx: Context, view: MenuView): Promise<void> {
|
||||
// Default to Markdown parse mode unless the menu explicitly opts out.
|
||||
// Views with user-supplied content set `parseMode: undefined` to render plain.
|
||||
const parseMode = "parseMode" in view ? view.parseMode : "Markdown";
|
||||
try {
|
||||
await ctx.editMessageText(view.text, {
|
||||
reply_markup: view.keyboard,
|
||||
parse_mode: "Markdown",
|
||||
parse_mode: parseMode,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.debug({ err }, "showMenu: edit failed, sending fresh message");
|
||||
await ctx.reply(view.text, {
|
||||
reply_markup: view.keyboard,
|
||||
parse_mode: "Markdown",
|
||||
parse_mode: parseMode,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -457,7 +460,7 @@ export async function showWizardConfirm(ctx: Context): Promise<void> {
|
||||
}
|
||||
const tz = op.defaultTimezone ?? DEFAULT_TIMEZONE;
|
||||
const whenLocal = DateTime.fromJSDate(w.scheduledAt).setZone(tz).toFormat("yyyy-MM-dd HH:mm");
|
||||
const body = w.text || (w.mediaId ? `[media${w.caption ? ` -- ${w.caption}` : ""}]` : "(empty)");
|
||||
const body = w.text || (w.mediaId ? `(media${w.caption ? ` — ${w.caption}` : ""})` : "(empty)");
|
||||
await showMenu(ctx, reminderConfirmMenu({
|
||||
accountLabel: account.label,
|
||||
groupName: group.name,
|
||||
|
||||
@ -21,6 +21,12 @@ import { DateTime } from "luxon";
|
||||
export type MenuView = {
|
||||
text: string;
|
||||
keyboard: InlineKeyboard;
|
||||
/**
|
||||
* Telegram parse mode. Defaults to "Markdown" for legacy formatting.
|
||||
* Set to undefined when the text includes user-supplied content that
|
||||
* could contain `*`, `_`, `` ` ``, `[`, `]` etc. and break the parser.
|
||||
*/
|
||||
parseMode?: "Markdown" | "MarkdownV2" | undefined;
|
||||
};
|
||||
|
||||
export function mainMenu(): MenuView {
|
||||
@ -283,7 +289,7 @@ export async function reminderDetailMenu(
|
||||
? DateTime.fromJSDate(rem.scheduledAt).setZone(operatorTimezone).toFormat("yyyy-MM-dd HH:mm")
|
||||
: "—";
|
||||
const messagePreview = rem.messages
|
||||
.map((m) => (m.kind === "text" ? m.textContent : `[${m.kind}]`))
|
||||
.map((m) => (m.kind === "text" ? m.textContent : `(${m.kind})`))
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
@ -293,14 +299,17 @@ export async function reminderDetailMenu(
|
||||
.text("⬅ Reminders", "m:reminders")
|
||||
.text("⬅ Main Menu", "m:main");
|
||||
|
||||
// User-supplied content (rem.name, messagePreview) — render as plain text
|
||||
// so stray `*` / `_` / `[` don't break Telegram's Markdown parser.
|
||||
return {
|
||||
text:
|
||||
`📅 *${rem.name}*\n\n` +
|
||||
`When: \`${when}\` (${rem.timezone})\n` +
|
||||
`Status: \`${rem.status}\`\n` +
|
||||
`📅 ${rem.name}\n\n` +
|
||||
`When: ${when} (${rem.timezone})\n` +
|
||||
`Status: ${rem.status}\n` +
|
||||
`Targets: ${rem.targets.length}\n\n` +
|
||||
`*Body:*\n${messagePreview || "(empty)"}`,
|
||||
`Body:\n${messagePreview || "(empty)"}`,
|
||||
keyboard,
|
||||
parseMode: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@ -455,13 +464,17 @@ export function reminderConfirmMenu(summary: {
|
||||
const keyboard = new InlineKeyboard()
|
||||
.text("✅ Schedule", "rm_save")
|
||||
.text("⬅ Cancel", "m:reminders");
|
||||
// Body is user-supplied — disable Markdown parsing to avoid stray `*`, `_`,
|
||||
// backticks, or `[` breaking the message. The trade-off is that the labels
|
||||
// ("Review", "Body") render as plain text, but that's fine for a confirm step.
|
||||
return {
|
||||
text:
|
||||
"*Review*\n\n" +
|
||||
"Review\n\n" +
|
||||
`Account: ${summary.accountLabel}\n` +
|
||||
`Group: ${summary.groupName}\n` +
|
||||
`When: ${summary.whenLocal}\n\n` +
|
||||
`*Body:*\n${summary.body}`,
|
||||
`Body:\n${summary.body}`,
|
||||
keyboard,
|
||||
parseMode: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user