Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions notify/telegram/telegram.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,19 @@ import (
"os"
"strconv"
"strings"
"unicode/utf8"

commoncfg "github.com/prometheus/common/config"
"golang.org/x/net/html"
"gopkg.in/telebot.v3"

"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)

// Telegram supports 4096 chars max - from https://limits.tginfo.me/en.
// Telegram supports up to 4096 characters after entity parsing.
// See https://core.telegram.org/bots/api#sendmessage.
const maxMessageLenRunes = 4096

// Notifier implements a Notifier for telegram notifications.
Expand Down Expand Up @@ -92,7 +95,7 @@ func (n *Notifier) Notify(ctx context.Context, alert ...*types.Alert) (bool, err
if err != nil {
return false, err
}
if len([]rune(messageText)) > maxMessageLenRunes {
if htmlTextRuneCount(messageText) > maxMessageLenRunes {
messageText = `Alertmanager notification could not be sent: message length exceeds Telegram limits.
Please check the template used for producing the message content.`
}
Expand Down Expand Up @@ -147,6 +150,26 @@ func wrapWithFailureReason(err error) error {
return err
}

// htmlTextRuneCount excludes HTML markup and attribute values such as link targets.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you find the corresponding source code? I don't feel comfortable just trusting the documentation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Verified against source. The Bot API calls parseTextEntities in Client.cpp. TDLib HTML parsing stores href separately as entity data and removes markup from resulting text in MessageEntity.cpp. The 4096 limit is then checked against parsed text in MessageContent.cpp. Thus the embedded link target is excluded from that limit. There is also a separate 32768-byte raw-input guard before parsing; this regression case remains below it.

func htmlTextRuneCount(message string) int {
tokenizer := html.NewTokenizer(strings.NewReader(message))
count := 0

for {
switch tokenizer.Next() {
case html.ErrorToken:
if len(tokenizer.Raw()) > 0 {
return utf8.RuneCountInString(message)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
return count

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems like the wrong with to return here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

x/net/html uses ErrorToken for normal EOF, so returning the accumulated count remains correct when Raw() is empty. An incomplete token can also return ErrorToken with unconsumed raw bytes, which made this path return a partial count. Commit fc7887d detects that case and conservatively returns the raw rune count instead.

case html.CommentToken, html.DoctypeToken:
return utf8.RuneCountInString(message)
case html.TextToken:
count += utf8.RuneCount(tokenizer.Text())
}
}
}

func createTelegramClient(apiURL, parseMode string, httpClient *http.Client) (*telebot.Bot, error) {
bot, err := telebot.NewBot(telebot.Settings{
URL: apiURL,
Expand Down
50 changes: 50 additions & 0 deletions notify/telegram/telegram_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ func TestTelegramRetry(t *testing.T) {

func TestTelegramNotify(t *testing.T) {
token := "secret"
longHTMLLink := `<a href="https://example.com/` + strings.Repeat("x", maxMessageLenRunes) + `">Open</a>`
longMalformedHTML := `<a href="` + strings.Repeat("x", maxMessageLenRunes)
longMalformedHTMLTemplate := fmt.Sprintf(`{{ %q | safeHtml }}`, longMalformedHTML)
longMalformedComment := `<!--` + strings.Repeat("x", maxMessageLenRunes)
longMalformedCommentTemplate := fmt.Sprintf(`{{ %q | safeHtml }}`, longMalformedComment)
longMalformedDoctype := `<!DOCTYPE ` + strings.Repeat("x", maxMessageLenRunes)
longMalformedDoctypeTemplate := fmt.Sprintf(`{{ %q | safeHtml }}`, longMalformedDoctype)

fileWithToken, err := os.CreateTemp(t.TempDir(), "telegram-bot-token")
require.NoError(t, err, "creating temp file failed")
Expand Down Expand Up @@ -133,6 +140,49 @@ func TestTelegramNotify(t *testing.T) {
expText: `Alertmanager notification could not be sent: message length exceeds Telegram limits.
Please check the template used for producing the message content.`,
},
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's add a test case for broken html too?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added in fc7887d. The regression renders a malformed anchor at the beginning through safeHtml, followed by more than 4096 raw runes. Before the fix, tokenization returned a partial count and the message was sent unchanged; it now takes the conservative fallback path.

name: "HTML mode ignores link targets when enforcing length limit",
cfg: TelegramConfig{
ParseMode: "HTML",
Message: longHTMLLink,
HTTPConfig: &commoncfg.HTTPClientConfig{},
BotToken: commoncfg.Secret(token),
},
expText: longHTMLLink,
},
{
name: "HTML mode falls back for too-large malformed tag",
cfg: TelegramConfig{
ParseMode: "HTML",
Message: longMalformedHTMLTemplate,
HTTPConfig: &commoncfg.HTTPClientConfig{},
BotToken: commoncfg.Secret(token),
},
expText: `Alertmanager notification could not be sent: message length exceeds Telegram limits.
Please check the template used for producing the message content.`,
},
{
name: "HTML mode falls back for too-large malformed comment",
cfg: TelegramConfig{
ParseMode: "HTML",
Message: longMalformedCommentTemplate,
HTTPConfig: &commoncfg.HTTPClientConfig{},
BotToken: commoncfg.Secret(token),
},
expText: `Alertmanager notification could not be sent: message length exceeds Telegram limits.
Please check the template used for producing the message content.`,
},
{
name: "HTML mode falls back for too-large malformed doctype",
cfg: TelegramConfig{
ParseMode: "HTML",
Message: longMalformedDoctypeTemplate,
HTTPConfig: &commoncfg.HTTPClientConfig{},
BotToken: commoncfg.Secret(token),
},
expText: `Alertmanager notification could not be sent: message length exceeds Telegram limits.
Please check the template used for producing the message content.`,
},
{
name: "Default mode with too-large message",
cfg: TelegramConfig{
Expand Down