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.
|
Status: beta.
|
||||||
|
|
||||||
### Changes
|
### 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.
|
- 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.
|
- 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).
|
- 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(sendVoice).toHaveBeenCalledTimes(1);
|
||||||
expect(sendMessage).not.toHaveBeenCalled();
|
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 linkPreviewOptions = linkPreviewEnabled ? undefined : { is_disabled: true };
|
||||||
const textMode = opts?.textMode ?? "markdown";
|
const textMode = opts?.textMode ?? "markdown";
|
||||||
const htmlText = textMode === "html" ? text : markdownToTelegramHtml(text);
|
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 {
|
try {
|
||||||
const res = await withTelegramApiErrorLogging({
|
const res = await withTelegramApiErrorLogging({
|
||||||
operation: "sendMessage",
|
operation: "sendMessage",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { markdownToTelegramHtml } from "./format.js";
|
import { markdownToTelegramChunks, markdownToTelegramHtml } from "./format.js";
|
||||||
|
|
||||||
describe("markdownToTelegramHtml", () => {
|
describe("markdownToTelegramHtml", () => {
|
||||||
it("renders basic inline formatting", () => {
|
it("renders basic inline formatting", () => {
|
||||||
@ -47,4 +47,39 @@ describe("markdownToTelegramHtml", () => {
|
|||||||
const res = markdownToTelegramHtml("```js\nconst x = 1;\n```");
|
const res = markdownToTelegramHtml("```js\nconst x = 1;\n```");
|
||||||
expect(res).toBe("<pre><code>const x = 1;\n</code></pre>");
|
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,
|
tableMode: options.tableMode,
|
||||||
});
|
});
|
||||||
const chunks = chunkMarkdownIR(ir, limit);
|
const chunks = chunkMarkdownIR(ir, limit);
|
||||||
return chunks.map((chunk) => ({
|
return (
|
||||||
html: renderTelegramHtml(chunk),
|
chunks
|
||||||
text: chunk.text,
|
.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[] {
|
export function markdownToTelegramHtmlChunks(markdown: string, limit: number): string[] {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user