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
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Alerting

-
- [FEATURE] Add an opt-in Telegram setting to send a single screenshot and short message as a photo with caption.

## Scope Glossary

Expand Down Expand Up @@ -43,4 +43,3 @@ Scopes must have an order to ensure consistency and ease of search, this helps u
3. `[BUGFIX]`
4. `[ENHANCEMENT]`
5. `[ADMIN]`

9 changes: 9 additions & 0 deletions receivers/telegram/v1/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Config struct {
DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty" yaml:"disable_web_page_preview,omitempty"`
ProtectContent bool `json:"protect_content,omitempty" yaml:"protect_content,omitempty"`
DisableNotifications bool `json:"disable_notifications,omitempty" yaml:"disable_notifications,omitempty"`
SendMessageAsCaption bool `json:"send_message_as_caption,omitempty" yaml:"send_message_as_caption,omitempty"`
}

func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Config, error) {
Expand All @@ -41,6 +42,7 @@ func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Confi
DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty" yaml:"disable_web_page_preview,omitempty"`
ProtectContent bool `json:"protect_content,omitempty" yaml:"protect_content,omitempty"`
DisableNotifications bool `json:"disable_notifications,omitempty" yaml:"disable_notifications,omitempty"`
SendMessageAsCaption bool `json:"send_message_as_caption,omitempty" yaml:"send_message_as_caption,omitempty"`
}{}
if err := json.Unmarshal(jsonData, &raw); err != nil {
return Config{}, fmt.Errorf("failed to unmarshal settings: %w", err)
Expand All @@ -52,6 +54,7 @@ func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Confi
DisableWebPagePreview: raw.DisableWebPagePreview,
ProtectContent: raw.ProtectContent,
DisableNotifications: raw.DisableNotifications,
SendMessageAsCaption: raw.SendMessageAsCaption,
}

settings.BotToken = decryptFn.Get("bottoken", raw.BotToken)
Expand Down Expand Up @@ -191,5 +194,11 @@ var Schema = schema.NewIntegrationSchemaVersion(schema.IntegrationSchemaVersion{
Element: schema.ElementTypeCheckbox,
PropertyName: "disable_notifications",
},
{
Label: "Send Message as Image Caption",
Description: "Sends a notification with one screenshot and a short message as a single Telegram photo with caption. Longer messages and notifications with multiple screenshots are sent separately.",
Element: schema.ElementTypeCheckbox,
PropertyName: "send_message_as_caption",
},
},
})
2 changes: 2 additions & 0 deletions receivers/telegram/v1/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ func TestNewConfig(t *testing.T) {
DisableWebPagePreview: true,
ProtectContent: true,
DisableNotifications: true,
SendMessageAsCaption: true,
},
},
{
Expand All @@ -144,6 +145,7 @@ func TestNewConfig(t *testing.T) {
DisableWebPagePreview: true,
ProtectContent: true,
DisableNotifications: true,
SendMessageAsCaption: true,
},
},
{
Expand Down
94 changes: 82 additions & 12 deletions receivers/telegram/v1/telegram.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"fmt"
"mime/multipart"
"unicode/utf8"

"github.com/go-kit/log/level"
"github.com/prometheus/alertmanager/notify"
Expand All @@ -24,6 +25,7 @@ var (

// Telegram supports 4096 chars max - from https://limits.tginfo.me/en.
const telegramMaxMessageLenRunes = 4096
const telegramMaxCaptionLenRunes = 1024

// Notifier is responsible for sending
// alert notifications to Telegram.
Expand Down Expand Up @@ -52,6 +54,16 @@ func New(cfg Config, meta receivers.Metadata, template *templates.Template, send
// Notify send an alert notification to Telegram.
func (tn *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
l := tn.GetLogger(ctx)

if tn.settings.SendMessageAsCaption {
sent, err := tn.notifyWithCaption(ctx, l, as)
if err != nil {
_ = level.Warn(l).Log("msg", "failed to send Telegram image with caption, falling back to separate messages", "error", err)
} else if sent {
return true, nil
}
}
Comment thread
cursor[bot] marked this conversation as resolved.

// Create the cmd for sendMessage
cmd, err := tn.newWebhookSyncCmd("sendMessage", func(w *multipart.Writer) error {
msg, err := tn.buildTelegramMessage(ctx, as, l)
Expand Down Expand Up @@ -79,18 +91,7 @@ func (tn *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error
return nil
}
cmd, err = tn.newWebhookSyncCmd("sendPhoto", func(w *multipart.Writer) error {
f, err := image.RawData(ctx)
if err != nil {
return fmt.Errorf("failed to open image: %w", err)
}
fw, err := w.CreateFormFile("photo", f.Name)
if err != nil {
return fmt.Errorf("failed to create form file: %w", err)
}
if _, err := fw.Write(f.Content); err != nil {
return fmt.Errorf("failed to write to form file: %w", err)
}
return nil
return tn.writeTelegramImage(ctx, w, image)
})
if err != nil {
return fmt.Errorf("failed to create image: %w", err)
Expand All @@ -105,6 +106,75 @@ func (tn *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error
return true, nil
}

func (tn *Notifier) notifyWithCaption(ctx context.Context, l log.Logger, as []*types.Alert) (bool, error) {
msg, err := tn.buildTelegramMessage(ctx, as, l)
if err != nil {
return false, fmt.Errorf("failed to build message: %w", err)
}
if utf8.RuneCountInString(msg["text"]) > telegramMaxCaptionLenRunes {
return false, nil
}

storedImages := make([]images.Image, 0, 1)
uploadedImages := make(map[string]struct{})
_ = images.WithStoredImages(ctx, l, tn.images, func(_ int, image images.Image) error {
if _, ok := uploadedImages[image.ID]; ok && image.ID != "" {
return nil
}
uploadedImages[image.ID] = struct{}{}
storedImages = append(storedImages, image)
if len(storedImages) > 1 {
return images.ErrImagesDone
}
return nil
}, as...)
if len(storedImages) != 1 {
return false, nil
Comment thread
zebrapurring marked this conversation as resolved.
}

cmd, err := tn.newWebhookSyncCmd("sendPhoto", func(w *multipart.Writer) error {
if err := tn.writeTelegramImage(ctx, w, storedImages[0]); err != nil {
return err
}
if err := w.WriteField("caption", msg["text"]); err != nil {
return fmt.Errorf("failed to create caption field: %w", err)
}
if parseMode := msg["parse_mode"]; parseMode != "" {
if err := w.WriteField("parse_mode", parseMode); err != nil {
return fmt.Errorf("failed to create parse mode field: %w", err)
}
}
if protectContent := msg["protect_content"]; protectContent != "" {
if err := w.WriteField("protect_content", protectContent); err != nil {
return fmt.Errorf("failed to create protect content field: %w", err)
}
}
return nil
})
if err != nil {
return false, fmt.Errorf("failed to create telegram image with caption: %w", err)
}
if err := tn.ns.SendWebhook(ctx, l, cmd); err != nil {
return false, fmt.Errorf("failed to send telegram image with caption: %w", err)
}
return true, nil
}

func (tn *Notifier) writeTelegramImage(ctx context.Context, w *multipart.Writer, image images.Image) error {
f, err := image.RawData(ctx)
if err != nil {
return fmt.Errorf("failed to open image: %w", err)
}
fw, err := w.CreateFormFile("photo", f.Name)
if err != nil {
return fmt.Errorf("failed to create form file: %w", err)
}
if _, err := fw.Write(f.Content); err != nil {
return fmt.Errorf("failed to write to form file: %w", err)
}
return nil
}

func (tn *Notifier) buildTelegramMessage(ctx context.Context, as []*types.Alert, l log.Logger) (map[string]string, error) {
var tmplErr error
defer func() {
Expand Down
Loading