diff --git a/pkg/channels/telegram/telegram.go b/pkg/channels/telegram/telegram.go index a28ae1bb9..c74eb20d5 100644 --- a/pkg/channels/telegram/telegram.go +++ b/pkg/channels/telegram/telegram.go @@ -184,23 +184,49 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err for _, chunk := range mdChunks { htmlContent := markdownToTelegramHTML(chunk) - tgMsg := tu.Message(tu.ID(chatID), htmlContent) - tgMsg.ParseMode = telego.ModeHTML - if _, err = c.bot.SendMessage(ctx, tgMsg); err != nil { - logger.ErrorCF("telegram", "HTML parse failed, falling back to plain text", map[string]any{ - "error": err.Error(), - }) - tgMsg.ParseMode = "" - if _, err = c.bot.SendMessage(ctx, tgMsg); err != nil { - return fmt.Errorf("telegram send: %w", channels.ErrTemporary) + // If HTML expansion pushes the chunk over Telegram's 4096-char limit, + // re-split the markdown chunk with a proportionally smaller maxLen. + if len([]rune(htmlContent)) > 4096 { + ratio := float64(len([]rune(chunk))) / float64(len([]rune(htmlContent))) + smallerLen := int(float64(4096) * ratio * 0.95) // 5% safety margin + if smallerLen < 100 { + smallerLen = 100 } + subChunks := channels.SplitMessage(chunk, smallerLen) + for _, sub := range subChunks { + if err := c.sendHTMLChunk(ctx, chatID, markdownToTelegramHTML(sub)); err != nil { + return err + } + } + continue + } + + if err := c.sendHTMLChunk(ctx, chatID, htmlContent); err != nil { + return err } } return nil } +// sendHTMLChunk sends a single HTML message, falling back to plain text on parse failure. +func (c *TelegramChannel) sendHTMLChunk(ctx context.Context, chatID int64, htmlContent string) error { + tgMsg := tu.Message(tu.ID(chatID), htmlContent) + tgMsg.ParseMode = telego.ModeHTML + + if _, err := c.bot.SendMessage(ctx, tgMsg); err != nil { + logger.ErrorCF("telegram", "HTML parse failed, falling back to plain text", map[string]any{ + "error": err.Error(), + }) + tgMsg.ParseMode = "" + if _, err = c.bot.SendMessage(ctx, tgMsg); err != nil { + return fmt.Errorf("telegram send: %w", channels.ErrTemporary) + } + } + return nil +} + // StartTyping implements channels.TypingCapable. // It sends ChatAction(typing) immediately and then repeats every 4 seconds // (Telegram's typing indicator expires after ~5s) in a background goroutine. diff --git a/pkg/channels/telegram/telegram_test.go b/pkg/channels/telegram/telegram_test.go index b93ea37ac..9d26bdd1a 100644 --- a/pkg/channels/telegram/telegram_test.go +++ b/pkg/channels/telegram/telegram_test.go @@ -196,6 +196,32 @@ func TestSend_LongMessage_HTMLFallback_StopsOnError(t *testing.T) { assert.Equal(t, 2, len(caller.calls), "should stop after first chunk fails both HTML and plain text") } +func TestSend_MarkdownShortButHTMLLong_MultipleCalls(t *testing.T) { + caller := &stubCaller{ + callFn: func(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) { + return successResponse(t), nil + }, + } + ch := newTestChannel(t, caller) + + // Create markdown whose length is <= 4096 but whose HTML expansion is much longer. + // "**a**" (5 chars) becomes "a" (8 chars) in HTML, so repeating it many times + // yields HTML that exceeds Telegram's limit while markdown stays within it. + markdownContent := strings.Repeat("**a** ", 700) // ~4200 chars markdown, but HTML ~5600+ chars + assert.LessOrEqual(t, len([]rune(markdownContent)), 4200, "markdown content should be near Telegram limit") + + htmlExpanded := markdownToTelegramHTML(markdownContent) + assert.Greater(t, len([]rune(htmlExpanded)), 4096, "HTML expansion must exceed Telegram limit for this test to be meaningful") + + err := ch.Send(context.Background(), bus.OutboundMessage{ + ChatID: "12345", + Content: markdownContent, + }) + + assert.NoError(t, err) + assert.Greater(t, len(caller.calls), 1, "markdown-short but HTML-long message should be split into multiple SendMessage calls") +} + func TestSend_NotRunning(t *testing.T) { caller := &stubCaller{ callFn: func(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) {