Skip to content

Commit

Permalink
Merge pull request #1308 from crazy-max/telegram-topics
Browse files Browse the repository at this point in the history
telegram: add topics support
  • Loading branch information
crazy-max authored Dec 18, 2024
2 parents 0990a69 + 804411d commit 4db25fc
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 28 deletions.
22 changes: 18 additions & 4 deletions docs/notif/telegram.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
```
Expand All @@ -25,7 +27,7 @@ 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 |
| `templateBody`[^1] | See [below](#default-templatebody) | [Notification template](../faq.md#notification-template) for message body |

Expand All @@ -37,7 +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"]`

### `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`

Expand Down
16 changes: 12 additions & 4 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,13 @@ for <code>{{ .Entry.Manifest.Platform }}</code> 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{
Expand Down Expand Up @@ -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,
},
},
Expand Down
6 changes: 4 additions & 2 deletions internal/config/fixtures/config.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions internal/config/fixtures/config.validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions internal/model/notif_telegram.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +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"`
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
Expand Down
91 changes: 80 additions & 11 deletions internal/notif/telegram/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package telegram
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"text/template"

Expand All @@ -21,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{
Expand All @@ -43,16 +49,27 @@ 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")
}
}
if len(cids) == 0 {
return errors.New("no chat IDs provided 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{
BotClient: &gotgbot.BaseBotClient{
Expand Down Expand Up @@ -90,15 +107,67 @@ func (c *Client) Send(entry model.NotifEntry) error {
return err
}

for _, chatID := range chatIDs {
_, err := bot.SendMessage(chatID, string(body), &gotgbot.SendMessageOpts{
ParseMode: gotgbot.ParseModeMarkdown,
LinkPreviewOptions: &gotgbot.LinkPreviewOptions{IsDisabled: true},
})
if err != nil {
return err
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 {
if err = sendTelegramMessage(bot, cid.id, 0, string(body)); err != nil {
return err
}
}
}

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,
ParseMode: gotgbot.ParseModeMarkdown,
LinkPreviewOptions: &gotgbot.LinkPreviewOptions{IsDisabled: true},
})
return err
}
84 changes: 84 additions & 0 deletions internal/notif/telegram/client_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}

0 comments on commit 4db25fc

Please sign in to comment.