diff --git a/docs/notif/telegram.md b/docs/notif/telegram.md index 6f70d674..b4197cc5 100644 --- a/docs/notif/telegram.md +++ b/docs/notif/telegram.md @@ -15,8 +15,10 @@ Multiple chat IDs can be provided in order to deliver notifications to multiple telegram: token: aabbccdd:11223344 chatIDs: - - 123456789 - - 987654321 + - "123456789" + - "987654321" + - "567891234:25" + - "891256734:25;12" templateBody: | Docker tag {{ .Entry.Image }} which you subscribed to through {{ .Entry.Provider }} provider has been released. ``` @@ -25,10 +27,8 @@ Multiple chat IDs can be provided in order to deliver notifications to multiple |--------------------|------------------------------------|---------------------------------------------------------------------------| | `token` | | Telegram bot token | | `tokenFile` | | Use content of secret file as Telegram bot token if `token` not defined | -| `chatIDs` | | List of chat IDs to send notifications to | +| `chatIDs` | | List of [chat IDs](#chatids-format) to send notifications to | | `chatIDsFile` | | Use content of secret file as chat IDs if `chatIDs` not defined | -| `chatTopics` | | List of chat topic IDs to send notifications to. | -| `chatTopicsFile` | | Use content of secret file as chat topic IDs if `chatTopics` not defined | | `templateBody`[^1] | See [below](#default-templatebody) | [Notification template](../faq.md#notification-template) for message body | !!! abstract "Environment variables" @@ -39,10 +39,19 @@ Multiple chat IDs can be provided in order to deliver notifications to multiple * `DIUN_NOTIF_TELEGRAM_TEMPLATEBODY` !!! example "chat IDs secret file" - Chat IDs secret file must be a valid JSON array like: `[123456789,987654321]` + Chat IDs secret file must be a valid JSON array like: `[123456789,987654321,"567891234:25","891256734:25;12"]` -!!! example "chat topic IDS secret file" - Chat topics is also an array, so you can specify a topic ID per chat ID: `[10,20]` +### `chatIDs` format + +Chat IDs can be provided in the following formats: + +* `123456789`: Send to chat ID `123456789` +* `567891234:25`: Send to chat ID `567891234` with target message topic `25` +* `891256734:25;12`: Send to chat ID `891256734` with target message topics `25` and `12` + +Each chat ID can be a simple integer or a string with additional topics. This +allows you to specify not only the chat ID but also the specific topics within +the chat to which the message should be sent. ### Default `templateBody` diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0c0b7add..1feedcfe 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -174,8 +174,13 @@ for {{ .Entry.Manifest.Platform }} platform. TemplateBody: model.NotifTeamsDefaultTemplateBody, }, Telegram: &model.NotifTelegram{ - Token: "abcdef123456", - ChatIDs: []int64{8547439, 1234567}, + Token: "abcdef123456", + ChatIDs: []string{ + "8547439", + "1234567", + "567891234:25", + "891256734:25;12", + }, TemplateBody: model.NotifTelegramDefaultTemplateBody, }, Webhook: &model.NotifWebhook{ @@ -333,8 +338,11 @@ func TestLoadEnv(t *testing.T) { Defaults: (&model.Defaults{}).GetDefaults(), Notif: &model.Notif{ Telegram: &model.NotifTelegram{ - Token: "abcdef123456", - ChatIDs: []int64{8547439, 1234567}, + Token: "abcdef123456", + ChatIDs: []string{ + "8547439", + "1234567", + }, TemplateBody: model.NotifTelegramDefaultTemplateBody, }, }, diff --git a/internal/config/fixtures/config.test.yml b/internal/config/fixtures/config.test.yml index 58d3c686..6d8b94ce 100644 --- a/internal/config/fixtures/config.test.yml +++ b/internal/config/fixtures/config.test.yml @@ -99,8 +99,10 @@ notif: telegram: token: abcdef123456 chatIDs: - - 8547439 - - 1234567 + - "8547439" + - "1234567" + - "567891234:25" + - "891256734:25;12" webhook: endpoint: http://webhook.foo.com/sd54qad89azd5a method: GET diff --git a/internal/config/fixtures/config.validate.yml b/internal/config/fixtures/config.validate.yml index f36c59d9..0c5576dc 100644 --- a/internal/config/fixtures/config.validate.yml +++ b/internal/config/fixtures/config.validate.yml @@ -91,8 +91,10 @@ notif: telegram: token: abcdef123456 chatIDs: - - 8547439 - - 1234567 + - "8547439" + - "1234567" + - "567891234:25" + - "891256734:25;12" webhook: endpoint: http://webhook.foo.com/sd54qad89azd5a method: GET diff --git a/internal/model/notif_telegram.go b/internal/model/notif_telegram.go index 67a6518b..33fe58a1 100644 --- a/internal/model/notif_telegram.go +++ b/internal/model/notif_telegram.go @@ -5,13 +5,11 @@ const NotifTelegramDefaultTemplateBody = `Docker tag {{ if .Entry.Image.HubLink // NotifTelegram holds Telegram notification configuration details type NotifTelegram struct { - Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"omitempty"` - TokenFile string `yaml:"tokenFile,omitempty" json:"tokenFile,omitempty" validate:"omitempty,file"` - ChatIDs []int64 `yaml:"chatIDs,omitempty" json:"chatIDs,omitempty" validate:"omitempty"` - ChatIDsFile string `yaml:"chatIDsFile,omitempty" json:"chatIDsFile,omitempty" validate:"omitempty,file"` - ChatTopics map[string][]int64 `yaml:"chatTopics,omitempty" json:"chatTopics,omitempty" validate:"omitempty"` - ChatTopicsFile string `yaml:"chatTopicsFile,omitempty" json:"chatTopicsFile,omitempty" validate:"omitempty,file"` - TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"` + Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"omitempty"` + TokenFile string `yaml:"tokenFile,omitempty" json:"tokenFile,omitempty" validate:"omitempty,file"` + ChatIDs []string `yaml:"chatIDs,omitempty" json:"chatIDs,omitempty" validate:"omitempty"` + ChatIDsFile string `yaml:"chatIDsFile,omitempty" json:"chatIDsFile,omitempty" validate:"omitempty,file"` + TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"` } // GetDefaults gets the default values diff --git a/internal/notif/telegram/client.go b/internal/notif/telegram/client.go index b465ab3a..3b884e2e 100644 --- a/internal/notif/telegram/client.go +++ b/internal/notif/telegram/client.go @@ -2,8 +2,8 @@ package telegram import ( "encoding/json" - "fmt" "net/http" + "strconv" "strings" "text/template" @@ -22,6 +22,11 @@ type Client struct { meta model.Meta } +type chatID struct { + id int64 + topics []int64 +} + // New creates a new Telegram notification instance func New(config *model.NotifTelegram, meta model.Meta) notifier.Notifier { return notifier.Notifier{ @@ -44,26 +49,26 @@ func (c *Client) Send(entry model.NotifEntry) error { return errors.Wrap(err, "cannot retrieve token secret for Telegram notifier") } - chatIDs := c.cfg.ChatIDs - chatIDsRaw, err := utl.GetSecret("", c.cfg.ChatIDsFile) + var cids []interface{} + for _, cid := range c.cfg.ChatIDs { + cids = append(cids, cid) + } + cidsRaw, err := utl.GetSecret("", c.cfg.ChatIDsFile) if err != nil { return errors.Wrap(err, "cannot retrieve chat IDs secret for Telegram notifier") } - if len(chatIDsRaw) > 0 { - if err = json.Unmarshal([]byte(chatIDsRaw), &chatIDs); err != nil { + if len(cidsRaw) > 0 { + if err = json.Unmarshal([]byte(cidsRaw), &cids); err != nil { return errors.Wrap(err, "cannot unmarshal chat IDs secret for Telegram notifier") } } - - chatTopics := c.cfg.ChatTopics - chatTopicsRaw, err := utl.GetSecret("", c.cfg.ChatTopicsFile) - if err != nil { - return errors.Wrap(err, "cannot retrieve chat topics secret for Telegram notifier") + if len(cids) == 0 { + return errors.New("no chat IDs provided for Telegram notifier") } - if len(chatTopicsRaw) > 0 { - if err = json.Unmarshal([]byte(chatTopicsRaw), &chatTopics); err != nil { - return errors.Wrap(err, "cannot unmarshal chat topics secret for Telegram notifier") - } + + parsedChatIDs, err := parseChatIDs(cids) + if err != nil { + return errors.Wrap(err, "cannot parse chat IDs for Telegram notifier") } bot, err := gotgbot.NewBot(token, &gotgbot.BotOpts{ @@ -102,17 +107,15 @@ func (c *Client) Send(entry model.NotifEntry) error { return err } - for _, chatID := range chatIDs { - if topics, ok := chatTopics[fmt.Sprintf("%d", chatID)]; ok { - for _, topic := range topics { - err = sendTelegramMessage(bot, chatID, topic, string(body)) - if err != nil { + for _, cid := range parsedChatIDs { + if len(cid.topics) > 0 { + for _, topic := range cid.topics { + if err = sendTelegramMessage(bot, cid.id, topic, string(body)); err != nil { return err } } } else { - err = sendTelegramMessage(bot, chatID, 0, string(body)) - if err != nil { + if err = sendTelegramMessage(bot, cid.id, 0, string(body)); err != nil { return err } } @@ -121,6 +124,45 @@ func (c *Client) Send(entry model.NotifEntry) error { return nil } +func parseChatIDs(entries []interface{}) ([]chatID, error) { + var chatIDs []chatID + for _, entry := range entries { + switch v := entry.(type) { + case int: + chatIDs = append(chatIDs, chatID{id: int64(v)}) + case int64: + chatIDs = append(chatIDs, chatID{id: v}) + case string: + parts := strings.Split(v, ":") + if len(parts) < 1 || len(parts) > 2 { + return nil, errors.Errorf("invalid chat ID %q", v) + } + id, err := strconv.ParseInt(parts[0], 10, 64) + if err != nil { + return nil, errors.Wrap(err, "invalid chat ID") + } + var topics []int64 + if len(parts) == 2 { + topicParts := strings.Split(parts[1], ";") + for _, topicPart := range topicParts { + topic, err := strconv.ParseInt(topicPart, 10, 64) + if err != nil { + return nil, errors.Wrapf(err, "invalid topic %q for chat ID %d", topicPart, id) + } + topics = append(topics, topic) + } + } + chatIDs = append(chatIDs, chatID{ + id: id, + topics: topics, + }) + default: + return nil, errors.Errorf("invalid chat ID %v (type=%T)", entry, entry) + } + } + return chatIDs, nil +} + func sendTelegramMessage(bot *gotgbot.Bot, chatID int64, threadID int64, message string) error { _, err := bot.SendMessage(chatID, message, &gotgbot.SendMessageOpts{ MessageThreadId: threadID, diff --git a/internal/notif/telegram/client_test.go b/internal/notif/telegram/client_test.go new file mode 100644 index 00000000..29fbdca5 --- /dev/null +++ b/internal/notif/telegram/client_test.go @@ -0,0 +1,84 @@ +package telegram + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +func TestParseChatIDs(t *testing.T) { + tests := []struct { + name string + entries []interface{} + expected []chatID + err error + }{ + { + name: "valid integers", + entries: []interface{}{8547439, 1234567}, + expected: []chatID{ + {id: 8547439}, + {id: 1234567}, + }, + err: nil, + }, + { + name: "valid strings with topics", + entries: []interface{}{"567891234:25", "891256734:25;12"}, + expected: []chatID{ + {id: 567891234, topics: []int64{25}}, + {id: 891256734, topics: []int64{25, 12}}, + }, + err: nil, + }, + { + name: "invalid format", + entries: []interface{}{"invalid_format"}, + expected: nil, + err: errors.New(`invalid chat ID: strconv.ParseInt: parsing "invalid_format": invalid syntax`), + }, + { + name: "invalid type", + entries: []interface{}{true}, + expected: nil, + err: errors.New("invalid chat ID true (type=bool)"), + }, + { + name: "empty string", + entries: []interface{}{""}, + expected: nil, + err: errors.New(`invalid chat ID: strconv.ParseInt: parsing "": invalid syntax`), + }, + { + name: "string with invalid topic", + entries: []interface{}{"567891234:invalid"}, + expected: nil, + err: errors.New(`invalid topic "invalid" for chat ID 567891234: strconv.ParseInt: parsing "invalid": invalid syntax`), + }, + { + name: "mixed valid and invalid entries", + entries: []interface{}{8547439, "567891234:25", "invalid_format", true}, + expected: nil, + err: errors.New(`invalid chat ID: strconv.ParseInt: parsing "invalid_format": invalid syntax`), + }, + { + name: "invalid format with too many parts", + entries: []interface{}{"567891234:25:extra"}, + expected: nil, + err: errors.New(`invalid chat ID "567891234:25:extra"`), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, err := parseChatIDs(tt.entries) + if tt.err != nil { + require.EqualError(t, err, tt.err.Error()) + } else { + require.NoError(t, err) + } + require.Equal(t, tt.expected, res) + }) + } +}