diff --git a/README.fr.md b/README.fr.md
index 2bec768fc..de0af0dd1 100644
--- a/README.fr.md
+++ b/README.fr.md
@@ -238,7 +238,8 @@ picoclaw onboard
"telegram": {
"enabled": true,
"token": "VOTRE_TOKEN_BOT",
- "allow_from": ["VOTRE_USER_ID"]
+ "allow_from": ["VOTRE_USER_ID"],
+ "use_markdown_v2": false
}
},
"tools": {
@@ -307,7 +308,8 @@ Discutez avec votre PicoClaw via Telegram, Discord, DingTalk, LINE ou WeCom
"telegram": {
"enabled": true,
"token": "VOTRE_TOKEN_BOT",
- "allow_from": ["VOTRE_USER_ID"]
+ "allow_from": ["VOTRE_USER_ID"],
+ "use_markdown_v2": false
}
}
}
@@ -350,7 +352,7 @@ picoclaw gateway
"discord": {
"enabled": true,
"token": "VOTRE_TOKEN_BOT",
- "allow_from": ["VOTRE_USER_ID"]
+ "*.md": ["VOTRE_USER_ID"]
}
}
}
@@ -866,7 +868,8 @@ picoclaw agent -m "Bonjour, comment ça va ?"
"telegram": {
"enabled": true,
"token": "123456:ABC...",
- "allow_from": ["123456789"]
+ "allow_from": ["123456789"],
+ "use_markdown_v2": false
},
"discord": {
"enabled": true,
diff --git a/README.ja.md b/README.ja.md
index 15ed1f649..c65a9006f 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -200,7 +200,8 @@ picoclaw onboard
"telegram": {
"enabled": true,
"token": "YOUR_TELEGRAM_BOT_TOKEN",
- "allow_from": []
+ "allow_from": [],
+ "use_markdown_v2": false
}
},
"tools": {
@@ -276,7 +277,8 @@ Telegram、Discord、QQ、DingTalk、LINE、WeCom で PicoClaw と会話でき
"telegram": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
- "allow_from": ["YOUR_USER_ID"]
+ "allow_from": ["YOUR_USER_ID"],
+ "use_markdown_v2": false
}
}
}
@@ -819,7 +821,8 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
"telegram": {
"enabled": true,
"token": "123456:ABC...",
- "allow_from": ["123456789"]
+ "allow_from": ["123456789"],
+ "use_markdown_v2": false
},
"discord": {
"enabled": true,
diff --git a/README.md b/README.md
index 2fc60343b..0ec8ef8e8 100644
--- a/README.md
+++ b/README.md
@@ -324,13 +324,15 @@ Talk to your picoclaw through Telegram, Discord, WhatsApp, DingTalk, LINE, or We
"telegram": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
- "allow_from": ["YOUR_USER_ID"]
+ "allow_from": ["YOUR_USER_ID"],
+ "use_markdown_v2": false
}
}
}
```
> Get your user ID from `@userinfobot` on Telegram.
+> Set `use_markdown_v2` to true could improve message formatting.
**3. Run**
@@ -1140,7 +1142,8 @@ picoclaw agent -m "Hello"
"telegram": {
"enabled": true,
"token": "123456:ABC...",
- "allow_from": ["123456789"]
+ "allow_from": ["123456789"],
+ "use_markdown_v2": false
},
"discord": {
"enabled": true,
diff --git a/README.pt-br.md b/README.pt-br.md
index 611a61281..8911cb680 100644
--- a/README.pt-br.md
+++ b/README.pt-br.md
@@ -301,7 +301,8 @@ Converse com seu PicoClaw via Telegram, Discord, DingTalk, LINE ou WeCom.
"telegram": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
- "allow_from": ["YOUR_USER_ID"]
+ "allow_from": ["YOUR_USER_ID"],
+ "use_markdown_v2": false
}
}
}
diff --git a/README.vi.md b/README.vi.md
index e836e30f0..dfc546074 100644
--- a/README.vi.md
+++ b/README.vi.md
@@ -219,7 +219,8 @@ picoclaw onboard
"telegram": {
"enabled": true,
"token": "YOUR_TELEGRAM_BOT_TOKEN",
- "allow_from": []
+ "allow_from": [],
+ "use_markdown_v2": false
}
}
}
@@ -275,7 +276,8 @@ Trò chuyện với PicoClaw qua Telegram, Discord, DingTalk, LINE hoặc WeCom.
"telegram": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
- "allow_from": ["YOUR_USER_ID"]
+ "allow_from": ["YOUR_USER_ID"],
+ "use_markdown_v2": false
}
}
}
@@ -834,7 +836,8 @@ picoclaw agent -m "Xin chào"
"telegram": {
"enabled": true,
"token": "123456:ABC...",
- "allow_from": ["123456789"]
+ "allow_from": ["123456789"],
+ "use_markdown_v2": false
},
"discord": {
"enabled": true,
diff --git a/README.zh.md b/README.zh.md
index 95984bbdf..3862d0719 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -713,7 +713,8 @@ picoclaw agent -m "你好"
"telegram": {
"enabled": true,
"token": "123456:ABC...",
- "allow_from": ["123456789"]
+ "allow_from": ["123456789"],
+ "use_markdown_v2": false
},
"discord": {
"enabled": true,
diff --git a/config/config.example.json b/config/config.example.json
index d885ef94b..1235d1b9d 100644
--- a/config/config.example.json
+++ b/config/config.example.json
@@ -50,9 +50,8 @@
"enabled": false,
"token": "YOUR_TELEGRAM_BOT_TOKEN",
"proxy": "",
- "allow_from": [
- "YOUR_USER_ID"
- ],
+ "allow_from": ["YOUR_USER_ID"],
+ "use_markdown_v2": false,
"reasoning_channel_id": ""
},
"discord": {
diff --git a/pkg/channels/telegram/parse_markdown_to_md_v2.go b/pkg/channels/telegram/parse_markdown_to_md_v2.go
new file mode 100644
index 000000000..1bebf700d
--- /dev/null
+++ b/pkg/channels/telegram/parse_markdown_to_md_v2.go
@@ -0,0 +1,160 @@
+package telegram
+
+import "strings"
+
+// markdownToTelegramMarkdownV2 takes a standardized markdown string and
+// strictly escapes or transforms it to fit Telegram's MarkdownV2 requirements.
+// https://core.telegram.org/bots/api#formatting-options
+func markdownToTelegramMarkdownV2(text string) string {
+ // replace Heading to bolding
+ text = reHeading.ReplaceAllString(text, "*$1*")
+
+ var result strings.Builder
+ runes := []rune(text)
+ length := len(runes)
+
+ // List of characters that must be escaped in standard text contexts
+ needsNormalEscape := func(r rune) bool {
+ switch r {
+ case '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!':
+ return true
+ }
+ return false
+ }
+
+ i := 0
+ for i < length {
+ // 1. Check for Pre-formatted Code Block (```...```)
+ if i+2 < length && runes[i] == '`' && runes[i+1] == '`' && runes[i+2] == '`' {
+ result.WriteString("```")
+ i += 3
+ // Find closing ```
+ for i < length {
+ if i+2 < length && runes[i] == '`' && runes[i+1] == '`' && runes[i+2] == '`' {
+ result.WriteString("```")
+ i += 3
+ break
+ }
+ // Inside code blocks, escape `\` and `\`
+ if runes[i] == '\\' || runes[i] == '`' {
+ result.WriteRune('\\')
+ }
+ result.WriteRune(runes[i])
+ i++
+ }
+ continue
+ }
+
+ // 2. Check for Inline Code (`...`)
+ if runes[i] == '`' {
+ result.WriteRune('`')
+ i++
+ for i < length {
+ if runes[i] == '`' {
+ result.WriteRune('`')
+ i++
+ break
+ }
+ if runes[i] == '\\' || runes[i] == '`' {
+ result.WriteRune('\\')
+ }
+ result.WriteRune(runes[i])
+ i++
+ }
+ continue
+ }
+
+ // 3. Link or Custom Emoji definition: URL part (...)
+ // We detect this by checking if the previous non-space character closed a bracket ']',
+ // and we are currently on '('. To keep logic linear, we handle it as we traverse.
+ // NOTE: A true deep-parser would link `[` to `](...)`. For safety, whenever we see `(`,
+ // if it looks like a URL part, we escape it via URL rules. Let's do a basic lookbehind.
+ if runes[i] == '(' && i > 0 && runes[i-1] == ']' {
+ result.WriteRune('(')
+ i++
+ for i < length {
+ if runes[i] == ')' {
+ // Unescaped closing bracket ends the URL
+ result.WriteRune(')')
+ i++
+ break
+ }
+ // In URL part, escape `\` and `)`
+ if runes[i] == '\\' || runes[i] == ')' {
+ result.WriteRune('\\')
+ }
+ result.WriteRune(runes[i])
+ i++
+ }
+ continue
+ }
+
+ // 4. Handle blockquotes starts
+ if runes[i] == '>' && (i == 0 || runes[i-1] == '\n') {
+ result.WriteRune('>')
+ i++
+ continue
+ }
+
+ // 5. Handle expandable block quotation starts
+ if runes[i] == '>' && runes[i-1] == '*' && runes[i-2] == '*' && (i == 0 || runes[i-3] == '\n') {
+ result.WriteRune(runes[i])
+ i++
+ continue
+ }
+
+ // 6. Handle standard Markdown Entities Boundaries
+ // If they are part of valid markdown boundaries, we write them as-is.
+ // We trust the syntax rules: * _ ~ || [ ]
+ // (Assuming the text is a valid markdown, we don't escape these if formatting is intended)
+
+ // Note on Ambiguity (__ vs _):
+ // Telegram parses `__` from left to right greedily.
+ if i+1 < length && runes[i] == '_' && runes[i+1] == '_' {
+ result.WriteString("__")
+ i += 2
+ continue
+ }
+
+ if i+1 < length && runes[i] == '|' && runes[i+1] == '|' {
+ result.WriteString("||")
+ i += 2
+ continue
+ }
+
+ // Standard single-char boundaries
+ if runes[i] == '*' || runes[i] == '_' || runes[i] == '~' || runes[i] == '[' || runes[i] == ']' {
+ result.WriteRune(runes[i])
+ i++
+ continue
+ }
+
+ // Custom emoji boundary check `![`
+ if i+1 < length && runes[i] == '!' && runes[i+1] == '[' {
+ result.WriteString("![")
+ i += 2
+ continue
+ }
+
+ // 7. Handle plain text characters
+ // Escape remaining special characters if they aren't forming intended valid markup
+ if needsNormalEscape(runes[i]) {
+ // Check if it's already escaped; if an escape character exists, consume it legitimately
+ if runes[i] == '\\' && i+1 < length && needsNormalEscape(runes[i+1]) {
+ // Keep the backslash and the escaped char as is, avoiding double escaping
+ result.WriteRune('\\')
+ result.WriteRune(runes[i+1])
+ i += 2
+ continue
+ }
+
+ // Auto-escape the character
+ result.WriteRune('\\')
+ }
+
+ result.WriteRune(runes[i])
+ i++
+ }
+
+ return result.String()
+}
diff --git a/pkg/channels/telegram/telegram_test.go b/pkg/channels/telegram/parse_markdown_to_md_v2_test.go
similarity index 69%
rename from pkg/channels/telegram/telegram_test.go
rename to pkg/channels/telegram/parse_markdown_to_md_v2_test.go
index 08ae14932..3d21a90c0 100644
--- a/pkg/channels/telegram/telegram_test.go
+++ b/pkg/channels/telegram/parse_markdown_to_md_v2_test.go
@@ -2,7 +2,6 @@ package telegram
import (
_ "embed"
- "fmt"
"testing"
"github.com/stretchr/testify/require"
@@ -13,29 +12,44 @@ var md2AllFormats string
func Test_markdownToTelegramMarkdownV2(t *testing.T) {
cases := []struct {
+ name string
input string
expected string
}{
{
+ name: "heading -> bolding",
input: `## HeadingH2 #`,
expected: "*HeadingH2 \\#*",
},
{
+ name: "strikethrough",
input: "~strikethroughMD~",
expected: "~strikethroughMD~",
},
{
+ name: "inline URL",
input: "[inline URL](http://www.example.com/)",
expected: "[inline URL](http://www.example.com/)",
},
{
+ name: "all telegram formats",
input: md2AllFormats,
expected: md2AllFormats,
},
+ {
+ name: "empty",
+ input: "",
+ expected: "",
+ },
+ {
+ name: "one letter",
+ input: "o",
+ expected: "o",
+ },
}
for _, tc := range cases {
- t.Run(fmt.Sprintf("formating %s -> %s", tc.input, tc.expected), func(t *testing.T) {
+ t.Run(tc.name, func(t *testing.T) {
actual := markdownToTelegramMarkdownV2(tc.input)
require.EqualValues(t, tc.expected, actual)
diff --git a/pkg/channels/telegram/parser_markdown_to_html.go b/pkg/channels/telegram/parser_markdown_to_html.go
new file mode 100644
index 000000000..bdaa51807
--- /dev/null
+++ b/pkg/channels/telegram/parser_markdown_to_html.go
@@ -0,0 +1,111 @@
+package telegram
+
+import (
+ "fmt"
+ "strings"
+)
+
+func markdownToTelegramHTML(text string) string {
+ if text == "" {
+ return ""
+ }
+
+ codeBlocks := extractCodeBlocks(text)
+ text = codeBlocks.text
+
+ inlineCodes := extractInlineCodes(text)
+ text = inlineCodes.text
+
+ text = reHeading.ReplaceAllString(text, "$1")
+
+ text = reBlockquote.ReplaceAllString(text, "$1")
+
+ text = escapeHTML(text)
+
+ text = reLink.ReplaceAllString(text, `$1`)
+
+ text = reBoldStar.ReplaceAllString(text, "$1")
+
+ text = reBoldUnder.ReplaceAllString(text, "$1")
+
+ text = reItalic.ReplaceAllStringFunc(text, func(s string) string {
+ match := reItalic.FindStringSubmatch(s)
+ if len(match) < 2 {
+ return s
+ }
+ return "" + match[1] + ""
+ })
+
+ text = reStrike.ReplaceAllString(text, "$1")
+
+ text = reListItem.ReplaceAllString(text, "• ")
+
+ for i, code := range inlineCodes.codes {
+ escaped := escapeHTML(code)
+ text = strings.ReplaceAll(text, fmt.Sprintf("\x00IC%d\x00", i), fmt.Sprintf("%s", escaped))
+ }
+
+ for i, code := range codeBlocks.codes {
+ escaped := escapeHTML(code)
+ text = strings.ReplaceAll(
+ text,
+ fmt.Sprintf("\x00CB%d\x00", i),
+ fmt.Sprintf("
%s", escaped),
+ )
+ }
+
+ return text
+}
+
+type codeBlockMatch struct {
+ text string
+ codes []string
+}
+
+func extractCodeBlocks(text string) codeBlockMatch {
+ matches := reCodeBlock.FindAllStringSubmatch(text, -1)
+
+ codes := make([]string, 0, len(matches))
+ for _, match := range matches {
+ codes = append(codes, match[1])
+ }
+
+ i := 0
+ text = reCodeBlock.ReplaceAllStringFunc(text, func(m string) string {
+ placeholder := fmt.Sprintf("\x00CB%d\x00", i)
+ i++
+ return placeholder
+ })
+
+ return codeBlockMatch{text: text, codes: codes}
+}
+
+type inlineCodeMatch struct {
+ text string
+ codes []string
+}
+
+func extractInlineCodes(text string) inlineCodeMatch {
+ matches := reInlineCode.FindAllStringSubmatch(text, -1)
+
+ codes := make([]string, 0, len(matches))
+ for _, match := range matches {
+ codes = append(codes, match[1])
+ }
+
+ i := 0
+ text = reInlineCode.ReplaceAllStringFunc(text, func(m string) string {
+ placeholder := fmt.Sprintf("\x00IC%d\x00", i)
+ i++
+ return placeholder
+ })
+
+ return inlineCodeMatch{text: text, codes: codes}
+}
+
+func escapeHTML(text string) string {
+ text = strings.ReplaceAll(text, "&", "&")
+ text = strings.ReplaceAll(text, "<", "<")
+ text = strings.ReplaceAll(text, ">", ">")
+ return text
+}
diff --git a/pkg/channels/telegram/telegram.go b/pkg/channels/telegram/telegram.go
index 9b439cc02..af0a4bbb8 100644
--- a/pkg/channels/telegram/telegram.go
+++ b/pkg/channels/telegram/telegram.go
@@ -25,7 +25,18 @@ import (
"github.com/sipeed/picoclaw/pkg/utils"
)
-var reHeading = regexp.MustCompile(`(?m)^#{1,6}\s+([^\n]+)`)
+var (
+ reHeading = regexp.MustCompile(`(?m)^#{1,6}\s+([^\n]+)`)
+ reBlockquote = regexp.MustCompile(`^>\s*(.*)$`)
+ reLink = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
+ reBoldStar = regexp.MustCompile(`\*\*(.+?)\*\*`)
+ reBoldUnder = regexp.MustCompile(`__(.+?)__`)
+ reItalic = regexp.MustCompile(`_([^_]+)_`)
+ reStrike = regexp.MustCompile(`~~(.+?)~~`)
+ reListItem = regexp.MustCompile(`^[-*]\s+`)
+ reCodeBlock = regexp.MustCompile("```[\\w]*\\n?([\\s\\S]*?)```")
+ reInlineCode = regexp.MustCompile("`([^`]+)`")
+)
type TelegramChannel struct {
*channels.BaseChannel
@@ -162,15 +173,13 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
return fmt.Errorf("invalid chat ID %s: %w", msg.ChatID, channels.ErrSendFailed)
}
- markdownV2Content := markdownToTelegramMarkdownV2(msg.Content)
+ content := c.parseContent(msg.Content)
- tgMsg := tu.Message(tu.ID(chatID), markdownV2Content).
+ tgMsg := tu.Message(tu.ID(chatID), content).
WithParseMode(telego.ModeMarkdownV2)
if _, err = c.bot.SendMessage(ctx, tgMsg); err != nil {
- logger.ErrorCF("telegram", "MarkdownV2 parse failed, falling back to plain text", map[string]any{
- "error": err.Error(),
- })
+ logParseFailed(c.config.Channels.Telegram.UseMarkdownV2, err)
if _, err = c.bot.SendMessage(ctx, tu.Message(tu.ID(chatID), msg.Content)); err != nil {
return fmt.Errorf("telegram send: %w", channels.ErrTemporary)
}
@@ -219,10 +228,15 @@ func (c *TelegramChannel) EditMessage(ctx context.Context, chatID string, messag
if err != nil {
return err
}
- md2Content := markdownToTelegramMarkdownV2(content)
- editMsg := tu.EditMessageText(tu.ID(cid), mid, md2Content).
+ parsedContent := c.parseContent(content)
+ editMsg := tu.EditMessageText(tu.ID(cid), mid, parsedContent).
WithParseMode(telego.ModeMarkdownV2)
_, err = c.bot.EditMessageText(ctx, editMsg)
+ if err != nil {
+ logParseFailed(c.config.Channels.Telegram.UseMarkdownV2, err)
+ _, err = c.bot.EditMessageText(ctx, tu.EditMessageText(tu.ID(cid), mid, content))
+ }
+
return err
}
@@ -535,169 +549,33 @@ func (c *TelegramChannel) downloadFile(ctx context.Context, fileID, ext string)
return c.downloadFileWithInfo(file, ext)
}
+func (c *TelegramChannel) parseContent(text string) string {
+ if c.config.Channels.Telegram.UseMarkdownV2 {
+ return markdownToTelegramMarkdownV2(text)
+ }
+
+ return markdownToTelegramHTML(text)
+}
+
+func logParseFailed(useMarkdownV2 bool, err error) {
+ parsingName := "HTML"
+ if useMarkdownV2 {
+ parsingName = "MarkdownV2"
+ }
+ logger.ErrorCF("telegram",
+ fmt.Sprintf("%s parse failed, falling back to plain text", parsingName),
+ map[string]any{
+ "error": err.Error(),
+ },
+ )
+}
+
func parseChatID(chatIDStr string) (int64, error) {
var id int64
_, err := fmt.Sscanf(chatIDStr, "%d", &id)
return id, err
}
-// markdownToTelegramMarkdownV2 takes a standardized markdown string and
-// strictly escapes or transforms it to fit Telegram's MarkdownV2 requirements.
-// https://core.telegram.org/bots/api#formatting-options
-func markdownToTelegramMarkdownV2(text string) string {
- // replace Heading to bolding
- text = reHeading.ReplaceAllString(text, "*$1*")
-
- var result strings.Builder
- runes := []rune(text)
- length := len(runes)
-
- // List of characters that must be escaped in standard text contexts
- needsNormalEscape := func(r rune) bool {
- switch r {
- case '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!':
- return true
- }
- return false
- }
-
- i := 0
- for i < length {
- // 1. Check for Pre-formatted Code Block (```...```)
- if i+2 < length && runes[i] == '`' && runes[i+1] == '`' && runes[i+2] == '`' {
- result.WriteString("```")
- i += 3
- // Find closing ```
- for i < length {
- if i+2 < length && runes[i] == '`' && runes[i+1] == '`' && runes[i+2] == '`' {
- result.WriteString("```")
- i += 3
- break
- }
- // Inside code blocks, escape `\` and `\`
- if runes[i] == '\\' || runes[i] == '`' {
- result.WriteRune('\\')
- }
- result.WriteRune(runes[i])
- i++
- }
- continue
- }
-
- // 2. Check for Inline Code (`...`)
- if runes[i] == '`' {
- result.WriteRune('`')
- i++
- for i < length {
- if runes[i] == '`' {
- result.WriteRune('`')
- i++
- break
- }
- if runes[i] == '\\' || runes[i] == '`' {
- result.WriteRune('\\')
- }
- result.WriteRune(runes[i])
- i++
- }
- continue
- }
-
- // 3. Link or Custom Emoji definition: URL part (...)
- // We detect this by checking if the previous non-space character closed a bracket ']',
- // and we are currently on '('. To keep logic linear, we handle it as we traverse.
- // NOTE: A true deep-parser would link `[` to `](...)`. For safety, whenever we see `(`,
- // if it looks like a URL part, we escape it via URL rules. Let's do a basic lookbehind.
- if runes[i] == '(' && i > 0 && runes[i-1] == ']' {
- result.WriteRune('(')
- i++
- for i < length {
- if runes[i] == ')' {
- // Unescaped closing bracket ends the URL
- result.WriteRune(')')
- i++
- break
- }
- // In URL part, escape `\` and `)`
- if runes[i] == '\\' || runes[i] == ')' {
- result.WriteRune('\\')
- }
- result.WriteRune(runes[i])
- i++
- }
- continue
- }
-
- // 4. Handle blockquotes starts
- if runes[i] == '>' && (i == 0 || runes[i-1] == '\n') {
- result.WriteRune('>')
- i++
- continue
- }
-
- // 5. Handle expandable block quotation starts
- if runes[i] == '>' && runes[i-1] == '*' && runes[i-2] == '*' && (i == 0 || runes[i-3] == '\n') {
- result.WriteRune(runes[i])
- i++
- continue
- }
-
- // 6. Handle standard Markdown Entities Boundaries
- // If they are part of valid markdown boundaries, we write them as-is.
- // We trust the syntax rules: * _ ~ || [ ]
- // (Assuming the text is a valid markdown, we don't escape these if formatting is intended)
-
- // Note on Ambiguity (__ vs _):
- // Telegram parses `__` from left to right greedily.
- if i+1 < length && runes[i] == '_' && runes[i+1] == '_' {
- result.WriteString("__")
- i += 2
- continue
- }
-
- if i+1 < length && runes[i] == '|' && runes[i+1] == '|' {
- result.WriteString("||")
- i += 2
- continue
- }
-
- // Standard single-char boundaries
- if runes[i] == '*' || runes[i] == '_' || runes[i] == '~' || runes[i] == '[' || runes[i] == ']' {
- result.WriteRune(runes[i])
- i++
- continue
- }
-
- // Custom emoji boundary check `![`
- if i+1 < length && runes[i] == '!' && runes[i+1] == '[' {
- result.WriteString("![")
- i += 2
- continue
- }
-
- // 7. Handle plain text characters
- // Escape remaining special characters if they aren't forming intended valid markup
- if needsNormalEscape(runes[i]) {
- // Check if it's already escaped; if an escape character exists, consume it legitimately
- if runes[i] == '\\' && i+1 < length && needsNormalEscape(runes[i+1]) {
- // Keep the backslash and the escaped char as is, avoiding double escaping
- result.WriteRune('\\')
- result.WriteRune(runes[i+1])
- i += 2
- continue
- }
-
- // Auto-escape the character
- result.WriteRune('\\')
- }
-
- result.WriteRune(runes[i])
- i++
- }
-
- return result.String()
-}
-
// isBotMentioned checks if the bot is mentioned in the message via entities.
func (c *TelegramChannel) isBotMentioned(message *telego.Message) bool {
botUsername := c.bot.Username()
diff --git a/pkg/config/config.go b/pkg/config/config.go
index e6a842595..fe9eb5efa 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -242,6 +242,7 @@ type TelegramConfig struct {
Typing TypingConfig `json:"typing,omitempty"`
Placeholder PlaceholderConfig `json:"placeholder,omitempty"`
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_TELEGRAM_REASONING_CHANNEL_ID"`
+ UseMarkdownV2 bool `json:"use_markdown_v2" env:"PICOCLAW_CHANNELS_TELEGRAM_USE_MARKDOWN_V2"`
}
type FeishuConfig struct {
diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go
index 44f4de7e9..ca5406fdd 100644
--- a/pkg/config/defaults.go
+++ b/pkg/config/defaults.go
@@ -56,6 +56,7 @@ func DefaultConfig() *Config {
Enabled: true,
Text: "Thinking... 💭",
},
+ UseMarkdownV2: false,
},
Feishu: FeishuConfig{
Enabled: false,
diff --git a/pkg/migrate/sources/openclaw/openclaw_config.go b/pkg/migrate/sources/openclaw/openclaw_config.go
index 39ad48fad..a188fe8fe 100644
--- a/pkg/migrate/sources/openclaw/openclaw_config.go
+++ b/pkg/migrate/sources/openclaw/openclaw_config.go
@@ -132,11 +132,12 @@ type OpenClawChannels struct {
}
type OpenClawTelegramConfig struct {
- BotToken *string `json:"botToken"`
- AllowFrom []string `json:"allowFrom"`
- GroupPolicy *string `json:"groupPolicy"`
- DmPolicy *string `json:"dmPolicy"`
- Enabled *bool `json:"enabled"`
+ BotToken *string `json:"botToken"`
+ AllowFrom []string `json:"allowFrom"`
+ GroupPolicy *string `json:"groupPolicy"`
+ DmPolicy *string `json:"dmPolicy"`
+ Enabled *bool `json:"enabled"`
+ UseMarkdownV2 *bool `json:"useMarkdownV2"`
}
type OpenClawDiscordConfig struct {
@@ -637,10 +638,11 @@ type WhatsAppConfig struct {
}
type TelegramConfig struct {
- Enabled bool `json:"enabled"`
- Token string `json:"token"`
- Proxy string `json:"proxy"`
- AllowFrom []string `json:"allow_from"`
+ Enabled bool `json:"enabled"`
+ Token string `json:"token"`
+ Proxy string `json:"proxy"`
+ AllowFrom []string `json:"allow_from"`
+ UseMarkdownV2 bool `json:"use_markdown_v2"`
}
type FeishuConfig struct {
@@ -758,9 +760,11 @@ func (c *OpenClawConfig) convertChannels(warnings *[]string) ChannelsConfig {
if c.Channels.Telegram != nil {
enabled := c.Channels.Telegram.Enabled == nil || *c.Channels.Telegram.Enabled
+ useMarkdownV2 := c.Channels.Telegram.UseMarkdownV2 != nil && *c.Channels.Telegram.UseMarkdownV2
channels.Telegram = TelegramConfig{
- Enabled: enabled,
- AllowFrom: c.Channels.Telegram.AllowFrom,
+ Enabled: enabled,
+ AllowFrom: c.Channels.Telegram.AllowFrom,
+ UseMarkdownV2: useMarkdownV2,
}
if c.Channels.Telegram.BotToken != nil {
channels.Telegram.Token = *c.Channels.Telegram.BotToken