Skip to content

Telegram formatting#2

Merged
dzarlax merged 2 commits into
mainfrom
claude/add-mcp-update-command-76JJK
Mar 20, 2026
Merged

Telegram formatting#2
dzarlax merged 2 commits into
mainfrom
claude/add-mcp-update-command-76JJK

Conversation

@dzarlax

@dzarlax dzarlax commented Mar 20, 2026

Copy link
Copy Markdown
Collaborator

Note

Medium Risk
Moderate risk because it changes core Telegram response delivery logic (HTML conversion, splitting, fallbacks) and could affect message ordering/formatting or introduce truncation edge cases for long outputs.

Overview
Improves Telegram output formatting by converting Markdown checklist items in bullet lists into Unicode checkboxes (☐/☑) and adding tests for the new rendering.

Reworks sendResponse to prefer HTML messages, then split and send long responses in multiple HTML/plain chunks (with retry fallbacks), only falling back to sending a .md document when chunked delivery fails. Adds splitMessage (paragraph/line-aware) plus unit tests to ensure chunks respect Telegram’s 4096-byte limit.

Written by Cursor Bugbot for commit 3da2235. This will update automatically on new commits. Configure here.

claude added 2 commits March 20, 2026 09:09
…ating

Previously, responses >= 4096 chars were sent as a .md file, and if that
failed, truncated with "(response truncated)". Now long responses are
split at paragraph/line boundaries into multiple messages. File upload
is used as fallback only if chunk sending fails.

https://claude.ai/code/session_01VtMjyBkcnrX8AZDuPj67XT
Long messages were sent as files (or truncated plain text), bypassing
the markdown-to-HTML converter entirely. Now: split markdown into chunks,
convert each to HTML, verify each fits in Telegram's 4096 char limit
(accounting for HTML tag expansion), and send as multiple messages.

Also adds checkbox rendering: [ ] → ☐, [x] → ☑ in bullet lists.

Fallback chain: split HTML msgs → plain text → file → truncated.

https://claude.ai/code/session_01VtMjyBkcnrX8AZDuPj67XT
@dzarlax dzarlax merged commit cf95580 into main Mar 20, 2026
2 checks passed

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Hard break in splitMessage may split multi-byte UTF-8 characters
    • Added UTF-8 boundary check that backs up from maxLen until finding a valid start byte before splitting, ensuring all chunks contain valid UTF-8.

Create PR

Or push these changes by commenting:

@cursor push 8ee6ffb5b0
Preview (8ee6ffb5b0)
diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go
--- a/internal/telegram/handler.go
+++ b/internal/telegram/handler.go
@@ -12,6 +12,7 @@
 	"strings"
 	"sync"
 	"time"
+	"unicode/utf8"
 
 	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
 
@@ -849,9 +850,16 @@
 			continue
 		}
 
-		// Hard break at maxLen.
-		parts = append(parts, chunk)
-		text = text[maxLen:]
+		// Hard break at maxLen, but respect UTF-8 character boundaries.
+		breakAt := maxLen
+		for breakAt > 0 && !utf8.RuneStart(text[breakAt]) {
+			breakAt--
+		}
+		if breakAt == 0 {
+			breakAt = maxLen // Avoid empty chunk; shouldn't happen with valid UTF-8.
+		}
+		parts = append(parts, text[:breakAt])
+		text = text[breakAt:]
 	}
 	return parts
 }

diff --git a/internal/telegram/markdown_test.go b/internal/telegram/markdown_test.go
--- a/internal/telegram/markdown_test.go
+++ b/internal/telegram/markdown_test.go
@@ -2,6 +2,7 @@
 
 import (
 	"testing"
+	"unicode/utf8"
 )
 
 func TestMarkdown_PlainText(t *testing.T) {
@@ -205,6 +206,29 @@
 	}
 }
 
+func TestSplitMessage_UTF8Boundary(t *testing.T) {
+	// "日本語" is 9 bytes (3 chars × 3 bytes each). No newlines, so hard break is needed.
+	// Create a string that requires hard break in the middle of a multi-byte character.
+	text := "日本語日本語日本語" // 27 bytes, 9 characters
+	parts := splitMessage(text, 10)
+	if len(parts) < 2 {
+		t.Errorf("expected multiple parts, got %d: %v", len(parts), parts)
+	}
+	for i, p := range parts {
+		if !utf8.ValidString(p) {
+			t.Errorf("part %d is invalid UTF-8: %q", i, p)
+		}
+	}
+	// Verify all parts joined equal the original
+	joined := ""
+	for _, p := range parts {
+		joined += p
+	}
+	if joined != text {
+		t.Errorf("parts don't reassemble to original:\n got: %q\nwant: %q", joined, text)
+	}
+}
+
 // --- helpers ---
 
 func assertMD(t *testing.T, input, want string) {

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.


// Hard break at maxLen.
parts = append(parts, chunk)
text = text[maxLen:]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard break in splitMessage may split multi-byte UTF-8 characters

Low Severity

The hard-break fallback in splitMessage slices at a raw byte offset (text[:maxLen]), which can split a multi-byte UTF-8 character in half. This produces invalid UTF-8 in the resulting chunk, potentially causing garbled text in the Telegram message or an API rejection. The test suite already uses Cyrillic text, indicating multi-byte input is expected. Long paragraphs without newlines in CJK or other non-ASCII languages could trigger this path.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants