@@ -10,6 +10,7 @@ interface SendMessageWithMarkdownFallbackParams {
1010 api : SendMessageApi ;
1111 chatId : Parameters < SendMessageApi [ "sendMessage" ] > [ 0 ] ;
1212 text : string ;
13+ rawFallbackText ?: string ;
1314 options ?: TelegramSendMessageOptions ;
1415 parseMode ?: "Markdown" | "MarkdownV2" ;
1516}
@@ -19,6 +20,7 @@ interface EditMessageWithMarkdownFallbackParams {
1920 chatId : Parameters < EditMessageApi [ "editMessageText" ] > [ 0 ] ;
2021 messageId : Parameters < EditMessageApi [ "editMessageText" ] > [ 1 ] ;
2122 text : string ;
23+ rawFallbackText ?: string ;
2224 options ?: TelegramEditMessageOptions ;
2325 parseMode ?: "Markdown" | "MarkdownV2" ;
2426}
@@ -31,6 +33,58 @@ const MARKDOWN_PARSE_ERROR_MARKERS = [
3133 "bad request: can't parse" ,
3234] ;
3335
36+ const MARKDOWN_V2_RESERVED_CHARS = new Set ( [
37+ "_" ,
38+ "*" ,
39+ "[" ,
40+ "]" ,
41+ "(" ,
42+ ")" ,
43+ "~" ,
44+ "`" ,
45+ ">" ,
46+ "#" ,
47+ "+" ,
48+ "-" ,
49+ "=" ,
50+ "|" ,
51+ "{" ,
52+ "}" ,
53+ "." ,
54+ "!" ,
55+ "\\" ,
56+ ] ) ;
57+ const MARKDOWN_V2_ESCAPED_CHAR = / \\ ( [ _ \* \[ \] \( \) ~ ` > # + \- = | { } . ! \\ ] ) / g;
58+
59+ function escapeTelegramMarkdownV2 ( text : string ) : string {
60+ let result = "" ;
61+ let trailingBackslashes = 0 ;
62+
63+ for ( const char of text ) {
64+ if ( char === "\\" ) {
65+ result += char ;
66+ trailingBackslashes += 1 ;
67+ continue ;
68+ }
69+
70+ const isEscaped = trailingBackslashes % 2 === 1 ;
71+ trailingBackslashes = 0 ;
72+
73+ if ( MARKDOWN_V2_RESERVED_CHARS . has ( char ) && ! isEscaped ) {
74+ result += `\\${ char } ` ;
75+ continue ;
76+ }
77+
78+ result += char ;
79+ }
80+
81+ return result ;
82+ }
83+
84+ function unescapeTelegramMarkdownV2 ( text : string ) : string {
85+ return text . replace ( MARKDOWN_V2_ESCAPED_CHAR , "$1" ) ;
86+ }
87+
3488function stripMarkdownFormattingOptions <
3589 T extends TelegramSendMessageOptions | TelegramEditMessageOptions | undefined ,
3690> ( options : T ) : T {
@@ -94,6 +148,7 @@ export async function sendMessageWithMarkdownFallback({
94148 api,
95149 chatId,
96150 text,
151+ rawFallbackText,
97152 options,
98153 parseMode,
99154} : SendMessageWithMarkdownFallbackParams ) : Promise <
@@ -108,15 +163,42 @@ export async function sendMessageWithMarkdownFallback({
108163 parse_mode : parseMode ,
109164 } ;
110165
166+ const fallbackText =
167+ rawFallbackText ?? ( parseMode === "MarkdownV2" ? unescapeTelegramMarkdownV2 ( text ) : text ) ;
168+
111169 try {
112170 return await api . sendMessage ( chatId , text , markdownOptions ) ;
113171 } catch ( error ) {
114172 if ( ! isTelegramMarkdownParseError ( error ) ) {
115173 throw error ;
116174 }
117175
176+ if ( parseMode === "MarkdownV2" ) {
177+ const escapedText = escapeTelegramMarkdownV2 ( text ) ;
178+ if ( escapedText !== text ) {
179+ logger . warn (
180+ "[Bot] Markdown parse failed, retrying assistant message with escaped MarkdownV2" ,
181+ error ,
182+ ) ;
183+
184+ try {
185+ return await api . sendMessage ( chatId , escapedText , markdownOptions ) ;
186+ } catch ( escapedError ) {
187+ if ( ! isTelegramMarkdownParseError ( escapedError ) ) {
188+ throw escapedError ;
189+ }
190+
191+ logger . warn (
192+ "[Bot] Escaped Markdown parse failed, retrying assistant message in raw mode" ,
193+ escapedError ,
194+ ) ;
195+ return api . sendMessage ( chatId , fallbackText , stripMarkdownFormattingOptions ( options ) ) ;
196+ }
197+ }
198+ }
199+
118200 logger . warn ( "[Bot] Markdown parse failed, retrying assistant message in raw mode" , error ) ;
119- return api . sendMessage ( chatId , text , stripMarkdownFormattingOptions ( options ) ) ;
201+ return api . sendMessage ( chatId , fallbackText , stripMarkdownFormattingOptions ( options ) ) ;
120202 }
121203}
122204
@@ -125,6 +207,7 @@ export async function editMessageWithMarkdownFallback({
125207 chatId,
126208 messageId,
127209 text,
210+ rawFallbackText,
128211 options,
129212 parseMode,
130213} : EditMessageWithMarkdownFallbackParams ) : Promise <
@@ -139,14 +222,51 @@ export async function editMessageWithMarkdownFallback({
139222 parse_mode : parseMode ,
140223 } ;
141224
225+ const fallbackText =
226+ rawFallbackText ?? ( parseMode === "MarkdownV2" ? unescapeTelegramMarkdownV2 ( text ) : text ) ;
227+
142228 try {
143229 return await api . editMessageText ( chatId , messageId , text , markdownOptions ) ;
144230 } catch ( error ) {
145231 if ( ! isTelegramMarkdownParseError ( error ) ) {
146232 throw error ;
147233 }
148234
235+ if ( parseMode === "MarkdownV2" ) {
236+ const escapedText = escapeTelegramMarkdownV2 ( text ) ;
237+ if ( escapedText !== text ) {
238+ logger . warn (
239+ "[Bot] Markdown parse failed, retrying edited message with escaped MarkdownV2" ,
240+ error ,
241+ ) ;
242+
243+ try {
244+ return await api . editMessageText ( chatId , messageId , escapedText , markdownOptions ) ;
245+ } catch ( escapedError ) {
246+ if ( ! isTelegramMarkdownParseError ( escapedError ) ) {
247+ throw escapedError ;
248+ }
249+
250+ logger . warn (
251+ "[Bot] Escaped Markdown parse failed, retrying edited message in raw mode" ,
252+ escapedError ,
253+ ) ;
254+ return api . editMessageText (
255+ chatId ,
256+ messageId ,
257+ fallbackText ,
258+ stripMarkdownFormattingOptions ( options ) ,
259+ ) ;
260+ }
261+ }
262+ }
263+
149264 logger . warn ( "[Bot] Markdown parse failed, retrying edited message in raw mode" , error ) ;
150- return api . editMessageText ( chatId , messageId , text , stripMarkdownFormattingOptions ( options ) ) ;
265+ return api . editMessageText (
266+ chatId ,
267+ messageId ,
268+ fallbackText ,
269+ stripMarkdownFormattingOptions ( options ) ,
270+ ) ;
151271 }
152272}
0 commit comments