-
Notifications
You must be signed in to change notification settings - Fork 154
Split long Telegram responses into multiple messages #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ | |
| import org.commonmark.parser.Parser; | ||
| import org.commonmark.renderer.html.HtmlRenderer; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| import static java.util.Optional.ofNullable; | ||
|
|
@@ -29,6 +30,8 @@ public class TelegramChannel implements Channel, SpringLongPollingBot, LongPolli | |
|
|
||
| private static final Logger LOGGER = LoggerFactory.getLogger(TelegramChannel.class); | ||
|
|
||
| private static final int MAX_MESSAGE_LENGTH = 4000; | ||
|
|
||
| private static final Parser MARKDOWN_PARSER = Parser.builder().build(); | ||
| private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder() | ||
| .escapeHtml(true) | ||
|
|
@@ -96,6 +99,12 @@ public void sendMessage(String message) { | |
| } | ||
|
|
||
| public void sendMessage(long chatId, Integer messageThreadId, String message) { | ||
| for (String chunk : splitMessage(message, MAX_MESSAGE_LENGTH)) { | ||
| sendSingleMessage(chatId, messageThreadId, chunk); | ||
| } | ||
| } | ||
|
|
||
| private void sendSingleMessage(long chatId, Integer messageThreadId, String message) { | ||
| String formattedHtmlMessage = convertMarkdownToTelegramHtml(message); | ||
|
|
||
| SendMessage htmlMessage = SendMessage.builder() | ||
|
|
@@ -124,6 +133,36 @@ public void sendMessage(long chatId, Integer messageThreadId, String message) { | |
| } | ||
| } | ||
|
|
||
| private List<String> splitMessage(String message, int maxLength) { | ||
| if (message == null || message.length() <= maxLength) return List.of(message == null ? "" : message); | ||
|
|
||
| List<String> chunks = new ArrayList<>(); | ||
| StringBuilder current = new StringBuilder(); | ||
|
|
||
| for (String line : message.split("\n", -1)) { | ||
| while (line.length() > maxLength) { | ||
| flush(chunks, current); | ||
| chunks.add(line.substring(0, maxLength)); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can split a surrogate pair (emoji) → invalid string, Telegram rejects it. Check Character.isHighSurrogate(line.charAt(maxLength - 1)) and back off by one.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed: |
||
| line = line.substring(maxLength); | ||
| } | ||
| if (!current.isEmpty() && current.length() + 1 + line.length() > maxLength) { | ||
| flush(chunks, current); | ||
| } | ||
| if (!current.isEmpty()) current.append('\n'); | ||
| current.append(line); | ||
| } | ||
| flush(chunks, current); | ||
|
|
||
| return chunks; | ||
| } | ||
|
|
||
| private void flush(List<String> chunks, StringBuilder current) { | ||
| if (!current.isEmpty()) { | ||
| chunks.add(current.toString()); | ||
| current.setLength(0); | ||
| } | ||
| } | ||
|
|
||
| private String convertMarkdownToTelegramHtml(String markdown) { | ||
| if (markdown == null || markdown.isBlank()) return ""; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Splitting happens on raw markdown, but chunks are HTML-converted after escaping (< → <) and tags (** → ) make the rendered text longer, so a 4000-char chunk can still exceed 4096 and get rejected. Either measure rendered length when chunking
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed: chunk boundaries are now chosen by binary-searching the HTML-rendered length (
renderedLength, which runs the markdown-to-HTML conversion) rather than the raw markdown length, so escaping/tag expansion is accounted for before a chunk is emitted. Added testsplitChunksRespectRenderedHtmlLengthNotRawMarkdownLengthcovering this.