fix(telegram): skip empty HTML chunks from thematic breaks (#3011)
Thematic breaks (---, ***, ___) and empty block-level markdown elements produce empty HTML after markdown→Telegram conversion. When these empty chunks are sent to the Telegram Bot API, it rejects with '400: message text is empty', which aborts the delivery loop and silently drops all subsequent message chunks. Fix: 1. markdownToTelegramChunks() now filters out chunks with empty HTML before returning, preventing empty chunks from reaching the delivery pipeline at all. 2. sendTelegramText() adds a defensive guard that skips empty HTML, protecting against any other code path that might produce one. Closes #3011
This commit is contained in:
parent
9688454a30
commit
84a60129be
@ -6,6 +6,7 @@ Docs: https://docs.molt.bot
|
||||
Status: beta.
|
||||
|
||||
### Changes
|
||||
- Telegram: skip empty HTML chunks from thematic breaks to prevent delivery loop abort. (#3011) Thanks @AlexAnys.
|
||||
- Rebrand: rename the npm package/CLI to `moltbot`, add a `moltbot` compatibility shim, and move extensions to the `@moltbot/*` scope.
|
||||
- Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev.
|
||||
- macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk).
|
||||
|
||||
@ -305,4 +305,32 @@ describe("deliverReplies", () => {
|
||||
expect(sendVoice).toHaveBeenCalledTimes(1);
|
||||
expect(sendMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips empty chunks from thematic breaks without crashing (#3011)", async () => {
|
||||
// A horizontal rule (---) in markdown produces empty HTML after conversion.
|
||||
// This must not cause a Telegram "message text is empty" 400 error or
|
||||
// abort delivery of subsequent chunks.
|
||||
const runtime = { error: vi.fn(), log: vi.fn() };
|
||||
const sendMessage = vi.fn().mockResolvedValue({ message_id: 1, chat: { id: "c" } });
|
||||
const bot = { api: { sendMessage } } as unknown as Bot;
|
||||
|
||||
await deliverReplies({
|
||||
replies: [{ text: "Before\n\n---\n\nAfter" }],
|
||||
chatId: "c",
|
||||
token: "tok",
|
||||
runtime,
|
||||
bot,
|
||||
replyToMode: "off",
|
||||
textLimit: 4000,
|
||||
});
|
||||
|
||||
// All sendMessage calls should have non-empty text
|
||||
for (const call of sendMessage.mock.calls) {
|
||||
const text = call[1] as string;
|
||||
expect(text.trim().length).toBeGreaterThan(0);
|
||||
}
|
||||
// "After" should not be silently dropped
|
||||
const allTexts = sendMessage.mock.calls.map((c: unknown[]) => c[1] as string).join(" ");
|
||||
expect(allTexts).toContain("After");
|
||||
});
|
||||
});
|
||||
|
||||
@ -476,6 +476,15 @@ async function sendTelegramText(
|
||||
const linkPreviewOptions = linkPreviewEnabled ? undefined : { is_disabled: true };
|
||||
const textMode = opts?.textMode ?? "markdown";
|
||||
const htmlText = textMode === "html" ? text : markdownToTelegramHtml(text);
|
||||
// Skip empty HTML chunks — thematic breaks (---/***/___) and empty
|
||||
// block-level elements produce zero visible output after markdown→HTML
|
||||
// conversion. Sending an empty string to the Telegram API triggers a
|
||||
// "400: message text is empty" error which aborts the entire delivery
|
||||
// loop, silently dropping all subsequent chunks. See #3011.
|
||||
if (!htmlText || !htmlText.trim()) {
|
||||
logVerbose("telegram sendTelegramText: skipping empty HTML chunk");
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const res = await withTelegramApiErrorLogging({
|
||||
operation: "sendMessage",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { markdownToTelegramHtml } from "./format.js";
|
||||
import { markdownToTelegramChunks, markdownToTelegramHtml } from "./format.js";
|
||||
|
||||
describe("markdownToTelegramHtml", () => {
|
||||
it("renders basic inline formatting", () => {
|
||||
@ -47,4 +47,39 @@ describe("markdownToTelegramHtml", () => {
|
||||
const res = markdownToTelegramHtml("```js\nconst x = 1;\n```");
|
||||
expect(res).toBe("<pre><code>const x = 1;\n</code></pre>");
|
||||
});
|
||||
|
||||
it("returns empty string for thematic break (---)", () => {
|
||||
const res = markdownToTelegramHtml("---");
|
||||
expect(res.trim()).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("markdownToTelegramChunks", () => {
|
||||
it("filters out empty chunks from thematic breaks", () => {
|
||||
// A thematic break (---) produces empty HTML; it must be filtered
|
||||
// out so Telegram doesn't receive an empty message. See #3011.
|
||||
const chunks = markdownToTelegramChunks("First paragraph\n\n---\n\nSecond paragraph", 4000);
|
||||
// Every returned chunk must have non-empty html
|
||||
for (const chunk of chunks) {
|
||||
expect(chunk.html.trim().length).toBeGreaterThan(0);
|
||||
}
|
||||
// Both paragraphs should still be present
|
||||
const combined = chunks.map((c) => c.html).join("");
|
||||
expect(combined).toContain("First paragraph");
|
||||
expect(combined).toContain("Second paragraph");
|
||||
});
|
||||
|
||||
it("filters out empty chunks from *** and ___", () => {
|
||||
const chunksAsterisk = markdownToTelegramChunks("Above\n\n***\n\nBelow", 4000);
|
||||
const chunksUnderscore = markdownToTelegramChunks("Above\n\n___\n\nBelow", 4000);
|
||||
for (const chunk of [...chunksAsterisk, ...chunksUnderscore]) {
|
||||
expect(chunk.html.trim().length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves normal chunks without filtering", () => {
|
||||
const chunks = markdownToTelegramChunks("hello **world**", 4000);
|
||||
expect(chunks.length).toBeGreaterThan(0);
|
||||
expect(chunks[0].html).toContain("hello");
|
||||
});
|
||||
});
|
||||
|
||||
@ -81,10 +81,18 @@ export function markdownToTelegramChunks(
|
||||
tableMode: options.tableMode,
|
||||
});
|
||||
const chunks = chunkMarkdownIR(ir, limit);
|
||||
return chunks.map((chunk) => ({
|
||||
html: renderTelegramHtml(chunk),
|
||||
text: chunk.text,
|
||||
}));
|
||||
return (
|
||||
chunks
|
||||
.map((chunk) => ({
|
||||
html: renderTelegramHtml(chunk),
|
||||
text: chunk.text,
|
||||
}))
|
||||
// Filter out empty chunks produced by thematic breaks (---/***/___),
|
||||
// empty blockquotes, or headings without text. These render to empty
|
||||
// HTML and would cause Telegram API "message text is empty" errors.
|
||||
// See #3011.
|
||||
.filter((chunk) => chunk.html.trim().length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
export function markdownToTelegramHtmlChunks(markdown: string, limit: number): string[] {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user