From 31e59fe44d18498ae53f624a3d3d5dbbad2d165e Mon Sep 17 00:00:00 2001 From: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com> Date: Sun, 21 Jun 2026 07:28:38 -0700 Subject: [PATCH] fix(telegram): preserve newlines in rich slash-command output (#46070) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bot API 10.1 sendRichMessage treats a lone newline as a soft break, so multi-line content joined with "\n".join(lines) — slash-command lists, etc. — collapses into a single paragraph. Normalize single newlines to Markdown hard breaks (two trailing spaces) in _rich_message_payload, leaving paragraph breaks and fenced code blocks untouched. Fixes #46070 --- plugins/platforms/telegram/adapter.py | 38 +++++- tests/gateway/test_telegram_rich_newlines.py | 118 +++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 tests/gateway/test_telegram_rich_newlines.py diff --git a/plugins/platforms/telegram/adapter.py b/plugins/platforms/telegram/adapter.py index fbc98c6edec..73431cd26bd 100644 --- a/plugins/platforms/telegram/adapter.py +++ b/plugins/platforms/telegram/adapter.py @@ -352,6 +352,38 @@ def _wrap_markdown_tables(text: str) -> str: return '\n'.join(out) +# --------------------------------------------------------------------------- +# Rich-message newline normalization +# --------------------------------------------------------------------------- + +# Matches fenced code blocks (```...\n...\n```), used to protect their +# content from newline normalization. +_RICH_CODE_FENCE_RE = re.compile(r'(```[^\n]*\n[\s\S]*?```)', re.MULTILINE) + + +def _rich_normalize_linebreaks(text: str) -> str: + """Convert single ``\\n`` to Markdown hard breaks for the rich-message path. + + Standard Markdown treats a lone ``\\n`` as whitespace (soft break), so + Bot API 10.1 ``sendRichMessage`` collapses multi-line content — e.g. + slash-command lists joined with ``"\\n".join(lines)`` — into a single + paragraph. Adding two trailing spaces before each single newline + forces a hard line break (``
``) in the rendered output. + + Paragraph breaks (``\\n\\n``) and fenced code blocks are left untouched. + """ + if not text or '\n' not in text: + return text + + parts = _RICH_CODE_FENCE_RE.split(text) + for i, part in enumerate(parts): + # Even indices are outside code fences; odd indices are fence content. + if i % 2 == 0: + # Convert single \n (not adjacent to another \n) to " \n". + parts[i] = re.sub(r'(?