Skip to content

Navigation Menu

Sign in
Appearance settings
Sign up
Appearance settings

[Notifier] Add TelegramOptions::disableMarkdownV2Escaping() - #65643

#65643
Closed
nicolas-grekas wants to merge 1 commit into
symfony:8.2symfony/symfony:8.2from
nicolas-grekas:telegram-markdownv2-escaping-opt-outnicolas-grekas/symfony:telegram-markdownv2-escaping-opt-outCopy head branch name to clipboard
Closed

[Notifier] Add TelegramOptions::disableMarkdownV2Escaping()#65643
nicolas-grekas wants to merge 1 commit into
symfony:8.2symfony/symfony:8.2from
nicolas-grekas:telegram-markdownv2-escaping-opt-outnicolas-grekas/symfony:telegram-markdownv2-escaping-opt-outCopy head branch name to clipboard

Conversation

@nicolas-grekas

Copy link
Copy Markdown
Member
Q A
Branch? 8.2
Bug fix? no
New feature? yes
Deprecations? no
Issues Fix #65626
License MIT

When the parse mode is not set or is MarkdownV2, TelegramTransport escapes the characters . ! # > + - = | { } ~ in the message text before sending it. This makes plain text work without any care from the caller. But >, || and ~ are MarkdownV2 markup (block quotation, spoiler, strikethrough), so these three formats cannot be used at all. Text that is already escaped by hand breaks too: 1\.2 is turned into 1\\.2, which Telegram rejects as an unescaped . after a literal backslash.

This PR adds TelegramOptions::disableMarkdownV2Escaping(bool $bool = true). When it is set, the transport sends the message text exactly as written. The text must then be valid MarkdownV2, including the escaping of the special characters that are not part of the markup. The default behavior is unchanged. The option is consumed by the transport and is not sent to the Telegram API. It has no effect with the HTML and Markdown parse modes, which are never escaped.

use Symfony\Component\Notifier\Bridge\Telegram\TelegramOptions;
use Symfony\Component\Notifier\Message\ChatMessage;

$options = (new TelegramOptions())
    ->parseMode(TelegramOptions::PARSE_MODE_MARKDOWN_V2)
    ->disableMarkdownV2Escaping();

$chatter->send(new ChatMessage(">Quote\n||Spoiler||\n~Strikethrough~\nVersion 1\\.2", $options));

Why an opt-out rather than a change of the escaped set: the set was already reshaped twice as a bug fix (#42721, #58636), and each change broke messages that relied on the previous behavior. Removing >, | and ~ from it would make a plain a > b or ~5 min fail with a 400 from Telegram after a patch release, and the same change would be needed again for every syntax Telegram adds (block quotations and spoilers were added to the Bot API after the escaping was written). The transport cannot tell markup from plain text; with this option, the caller decides.

Alternatives with the existing API, checked before adding the method:

  • parseMode('markdownv2') in lowercase, reported as a workaround in [Notifier] [Telegram] MARKDOWN_V2 is broken #51330, skips the escaping only because the transport compares the mode case-sensitively. It relies on an implementation detail and on Telegram accepting the value regardless of case.
  • parseMode(TelegramOptions::PARSE_MODE_HTML) supports <blockquote>, <tg-spoiler> and <s> and is never escaped, but it does not help anyone who writes MarkdownV2.

Checks run:

  • ./phpunit src/Symfony/Component/Notifier/Bridge/Telegram: OK (101 tests, 221 assertions).
  • Revert-verify: with the source changes reverted and the new tests kept, the 3 new tests error on the undefined method. With only the TelegramOptions method present, the two transport tests fail on the escaped text and on the option leaking into the request body.
  • php-cs-fixer fix --dry-run on the touched files: clean.

Not covered: no call to the real Telegram API was made.

For the documentation: the README of the bridge gets a section in this PR. notifier.rst has nothing on Telegram escaping today; it should get the same points: the default escaping and its character set, disableMarkdownV2Escaping(), and the rule that the text must then be valid MarkdownV2.

@nicolas-grekas

Copy link
Copy Markdown
Member Author

Closing in favor of #65644, which fixes the escaping itself on 6.4 instead of adding an opt-out.

@nicolas-grekas
nicolas-grekas deleted the telegram-markdownv2-escaping-opt-out branch August 25, 2026 14:01
nicolas-grekas added a commit that referenced this pull request Aug 25, 2026
…ansport (nicolas-grekas)

This PR was merged into the 6.4 branch.

Discussion
----------

[Notifier] Fix escaping of MarkdownV2 markup in TelegramTransport

| Q             | A
| ------------- | ---
| Branch?       | 6.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Issues        | Fix #65626
| License       | MIT

When the parse mode is not set or is `MarkdownV2`, `TelegramTransport` escapes reserved characters in the message text before sending it. The list of characters it escapes was adjusted by hand three times (#41600, #42721, #58636) and still contains three markup markers of MarkdownV2: `~` (strikethrough), `|` (`||spoiler||`) and `>` (block quotation). These formats cannot be used at all. Text that is already escaped by hand is escaped a second time (`1\.2` becomes `1\\.2`), which Telegram rejects.

This PR replaces the character list with the rule that Telegram's own parser applies: a reserved character is escaped when Telegram would reject it, and the markup is left alone.

- Paired markers are never escaped: `*bold*`, `_italic_`, `__underline__`, `` `code` ``, `~strikethrough~`, `||spoiler||`, `[link](url)`. The first five were already left alone since #58636; `~` and `||` now follow the same rule.
- `>` is left alone at the start of a line, where it starts a block quotation, including the `**>` form of expandable quotations. It is escaped anywhere else.
- `!` is left alone in front of `[`, where it starts a custom emoji or a date-time entity. It is escaped anywhere else.
- A character that is already escaped is kept as it is, so a message written as valid MarkdownV2 is sent unchanged.
- `.`, `#`, `+`, `-`, `=`, `{`, `}` and a single `|` are escaped as before.

It also fixes the character class of the old pattern: `+-=` was read as a range from `+` to `=`, so digits, `,`, `/`, `:`, `;` and `<` were escaped too. Telegram accepts a backslash in front of any of them, so nothing was visible in the chat, but every digit counted twice towards the 4096-character limit.

Behavior change for plain-text senders: a message with an unescaped `~` or `||`, or a line starting with `>`, is now sent as markup, the same way `*` and `_` are since #58636. Plain text is sent as before when these characters are escaped in the message, or with the `HTML` parse mode.

Checks run:

- `./phpunit src/Symfony/Component/Notifier/Bridge/Telegram` on 6.4: OK, 70 tests, 161 assertions, 1 skipped (pre-existing).
- Before the fix, the new case table fails on its first case and the existing escaping test fails on `\~`; both pass after it.
- `php-cs-fixer fix --dry-run` on the touched files: clean.

Not covered: no call to the real Telegram API was made. The rules follow the MarkdownV2 section of the Bot API documentation.

Replaces #65643.

Commits
-------

8ce13ef [Notifier] Fix escaping of MarkdownV2 markup in TelegramTransport
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Notifier][Telegram] Auto-escaping in TelegramTransport breaks MarkdownV2 blockquote (>), spoiler (||) and strikethrough (~)

2 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.