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
|
// 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).
|
// won't let us turn it back into a text message).
|
||||||
async function showMenu(ctx: Context, view: MenuView): Promise<void> {
|
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 {
|
try {
|
||||||
await ctx.editMessageText(view.text, {
|
await ctx.editMessageText(view.text, {
|
||||||
reply_markup: view.keyboard,
|
reply_markup: view.keyboard,
|
||||||
parse_mode: "Markdown",
|
parse_mode: parseMode,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.debug({ err }, "showMenu: edit failed, sending fresh message");
|
logger.debug({ err }, "showMenu: edit failed, sending fresh message");
|
||||||
await ctx.reply(view.text, {
|
await ctx.reply(view.text, {
|
||||||
reply_markup: view.keyboard,
|
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 tz = op.defaultTimezone ?? DEFAULT_TIMEZONE;
|
||||||
const whenLocal = DateTime.fromJSDate(w.scheduledAt).setZone(tz).toFormat("yyyy-MM-dd HH:mm");
|
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({
|
await showMenu(ctx, reminderConfirmMenu({
|
||||||
accountLabel: account.label,
|
accountLabel: account.label,
|
||||||
groupName: group.name,
|
groupName: group.name,
|
||||||
|
|||||||
@ -21,6 +21,12 @@ import { DateTime } from "luxon";
|
|||||||
export type MenuView = {
|
export type MenuView = {
|
||||||
text: string;
|
text: string;
|
||||||
keyboard: InlineKeyboard;
|
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 {
|
export function mainMenu(): MenuView {
|
||||||
@ -283,7 +289,7 @@ export async function reminderDetailMenu(
|
|||||||
? DateTime.fromJSDate(rem.scheduledAt).setZone(operatorTimezone).toFormat("yyyy-MM-dd HH:mm")
|
? DateTime.fromJSDate(rem.scheduledAt).setZone(operatorTimezone).toFormat("yyyy-MM-dd HH:mm")
|
||||||
: "—";
|
: "—";
|
||||||
const messagePreview = rem.messages
|
const messagePreview = rem.messages
|
||||||
.map((m) => (m.kind === "text" ? m.textContent : `[${m.kind}]`))
|
.map((m) => (m.kind === "text" ? m.textContent : `(${m.kind})`))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
@ -293,14 +299,17 @@ export async function reminderDetailMenu(
|
|||||||
.text("⬅ Reminders", "m:reminders")
|
.text("⬅ Reminders", "m:reminders")
|
||||||
.text("⬅ Main Menu", "m:main");
|
.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 {
|
return {
|
||||||
text:
|
text:
|
||||||
`📅 *${rem.name}*\n\n` +
|
`📅 ${rem.name}\n\n` +
|
||||||
`When: \`${when}\` (${rem.timezone})\n` +
|
`When: ${when} (${rem.timezone})\n` +
|
||||||
`Status: \`${rem.status}\`\n` +
|
`Status: ${rem.status}\n` +
|
||||||
`Targets: ${rem.targets.length}\n\n` +
|
`Targets: ${rem.targets.length}\n\n` +
|
||||||
`*Body:*\n${messagePreview || "(empty)"}`,
|
`Body:\n${messagePreview || "(empty)"}`,
|
||||||
keyboard,
|
keyboard,
|
||||||
|
parseMode: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -455,13 +464,17 @@ export function reminderConfirmMenu(summary: {
|
|||||||
const keyboard = new InlineKeyboard()
|
const keyboard = new InlineKeyboard()
|
||||||
.text("✅ Schedule", "rm_save")
|
.text("✅ Schedule", "rm_save")
|
||||||
.text("⬅ Cancel", "m:reminders");
|
.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 {
|
return {
|
||||||
text:
|
text:
|
||||||
"*Review*\n\n" +
|
"Review\n\n" +
|
||||||
`Account: ${summary.accountLabel}\n` +
|
`Account: ${summary.accountLabel}\n` +
|
||||||
`Group: ${summary.groupName}\n` +
|
`Group: ${summary.groupName}\n` +
|
||||||
`When: ${summary.whenLocal}\n\n` +
|
`When: ${summary.whenLocal}\n\n` +
|
||||||
`*Body:*\n${summary.body}`,
|
`Body:\n${summary.body}`,
|
||||||
keyboard,
|
keyboard,
|
||||||
|
parseMode: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user