Skip to content

Commit 4db25fc

Browse files
authored
Merge pull request #1308 from crazy-max/telegram-topics
telegram: add topics support
2 parents 0990a69 + 804411d commit 4db25fc

File tree

7 files changed

+207
-28
lines changed

7 files changed

+207
-28
lines changed

docs/notif/telegram.md

+18-4
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ Multiple chat IDs can be provided in order to deliver notifications to multiple
1515
telegram:
1616
token: aabbccdd:11223344
1717
chatIDs:
18-
- 123456789
19-
- 987654321
18+
- "123456789"
19+
- "987654321"
20+
- "567891234:25"
21+
- "891256734:25;12"
2022
templateBody: |
2123
Docker tag {{ .Entry.Image }} which you subscribed to through {{ .Entry.Provider }} provider has been released.
2224
```
@@ -25,7 +27,7 @@ Multiple chat IDs can be provided in order to deliver notifications to multiple
2527
|--------------------|------------------------------------|---------------------------------------------------------------------------|
2628
| `token` | | Telegram bot token |
2729
| `tokenFile` | | Use content of secret file as Telegram bot token if `token` not defined |
28-
| `chatIDs` | | List of chat IDs to send notifications to |
30+
| `chatIDs` | | List of [chat IDs](#chatids-format) to send notifications to |
2931
| `chatIDsFile` | | Use content of secret file as chat IDs if `chatIDs` not defined |
3032
| `templateBody`[^1] | See [below](#default-templatebody) | [Notification template](../faq.md#notification-template) for message body |
3133

@@ -37,7 +39,19 @@ Multiple chat IDs can be provided in order to deliver notifications to multiple
3739
* `DIUN_NOTIF_TELEGRAM_TEMPLATEBODY`
3840

3941
!!! example "chat IDs secret file"
40-
Chat IDs secret file must be a valid JSON array like: `[123456789,987654321]`
42+
Chat IDs secret file must be a valid JSON array like: `[123456789,987654321,"567891234:25","891256734:25;12"]`
43+
44+
### `chatIDs` format
45+
46+
Chat IDs can be provided in the following formats:
47+
48+
* `123456789`: Send to chat ID `123456789`
49+
* `567891234:25`: Send to chat ID `567891234` with target message topic `25`
50+
* `891256734:25;12`: Send to chat ID `891256734` with target message topics `25` and `12`
51+
52+
Each chat ID can be a simple integer or a string with additional topics. This
53+
allows you to specify not only the chat ID but also the specific topics within
54+
the chat to which the message should be sent.
4155

4256
### Default `templateBody`
4357

internal/config/config_test.go

+12-4
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,13 @@ for <code>{{ .Entry.Manifest.Platform }}</code> platform.
174174
TemplateBody: model.NotifTeamsDefaultTemplateBody,
175175
},
176176
Telegram: &model.NotifTelegram{
177-
Token: "abcdef123456",
178-
ChatIDs: []int64{8547439, 1234567},
177+
Token: "abcdef123456",
178+
ChatIDs: []string{
179+
"8547439",
180+
"1234567",
181+
"567891234:25",
182+
"891256734:25;12",
183+
},
179184
TemplateBody: model.NotifTelegramDefaultTemplateBody,
180185
},
181186
Webhook: &model.NotifWebhook{
@@ -333,8 +338,11 @@ func TestLoadEnv(t *testing.T) {
333338
Defaults: (&model.Defaults{}).GetDefaults(),
334339
Notif: &model.Notif{
335340
Telegram: &model.NotifTelegram{
336-
Token: "abcdef123456",
337-
ChatIDs: []int64{8547439, 1234567},
341+
Token: "abcdef123456",
342+
ChatIDs: []string{
343+
"8547439",
344+
"1234567",
345+
},
338346
TemplateBody: model.NotifTelegramDefaultTemplateBody,
339347
},
340348
},

internal/config/fixtures/config.test.yml

+4-2
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,10 @@ notif:
9999
telegram:
100100
token: abcdef123456
101101
chatIDs:
102-
- 8547439
103-
- 1234567
102+
- "8547439"
103+
- "1234567"
104+
- "567891234:25"
105+
- "891256734:25;12"
104106
webhook:
105107
endpoint: http://webhook.foo.com/sd54qad89azd5a
106108
method: GET

internal/config/fixtures/config.validate.yml

+4-2
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,10 @@ notif:
9191
telegram:
9292
token: abcdef123456
9393
chatIDs:
94-
- 8547439
95-
- 1234567
94+
- "8547439"
95+
- "1234567"
96+
- "567891234:25"
97+
- "891256734:25;12"
9698
webhook:
9799
endpoint: http://webhook.foo.com/sd54qad89azd5a
98100
method: GET

internal/model/notif_telegram.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ const NotifTelegramDefaultTemplateBody = `Docker tag {{ if .Entry.Image.HubLink
55

66
// NotifTelegram holds Telegram notification configuration details
77
type NotifTelegram struct {
8-
Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"omitempty"`
9-
TokenFile string `yaml:"tokenFile,omitempty" json:"tokenFile,omitempty" validate:"omitempty,file"`
10-
ChatIDs []int64 `yaml:"chatIDs,omitempty" json:"chatIDs,omitempty" validate:"omitempty"`
11-
ChatIDsFile string `yaml:"chatIDsFile,omitempty" json:"chatIDsFile,omitempty" validate:"omitempty,file"`
12-
TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"`
8+
Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"omitempty"`
9+
TokenFile string `yaml:"tokenFile,omitempty" json:"tokenFile,omitempty" validate:"omitempty,file"`
10+
ChatIDs []string `yaml:"chatIDs,omitempty" json:"chatIDs,omitempty" validate:"omitempty"`
11+
ChatIDsFile string `yaml:"chatIDsFile,omitempty" json:"chatIDsFile,omitempty" validate:"omitempty,file"`
12+
TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"`
1313
}
1414

1515
// GetDefaults gets the default values

internal/notif/telegram/client.go

+80-11
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package telegram
33
import (
44
"encoding/json"
55
"net/http"
6+
"strconv"
67
"strings"
78
"text/template"
89

@@ -21,6 +22,11 @@ type Client struct {
2122
meta model.Meta
2223
}
2324

25+
type chatID struct {
26+
id int64
27+
topics []int64
28+
}
29+
2430
// New creates a new Telegram notification instance
2531
func New(config *model.NotifTelegram, meta model.Meta) notifier.Notifier {
2632
return notifier.Notifier{
@@ -43,16 +49,27 @@ func (c *Client) Send(entry model.NotifEntry) error {
4349
return errors.Wrap(err, "cannot retrieve token secret for Telegram notifier")
4450
}
4551

46-
chatIDs := c.cfg.ChatIDs
47-
chatIDsRaw, err := utl.GetSecret("", c.cfg.ChatIDsFile)
52+
var cids []interface{}
53+
for _, cid := range c.cfg.ChatIDs {
54+
cids = append(cids, cid)
55+
}
56+
cidsRaw, err := utl.GetSecret("", c.cfg.ChatIDsFile)
4857
if err != nil {
4958
return errors.Wrap(err, "cannot retrieve chat IDs secret for Telegram notifier")
5059
}
51-
if len(chatIDsRaw) > 0 {
52-
if err = json.Unmarshal([]byte(chatIDsRaw), &chatIDs); err != nil {
60+
if len(cidsRaw) > 0 {
61+
if err = json.Unmarshal([]byte(cidsRaw), &cids); err != nil {
5362
return errors.Wrap(err, "cannot unmarshal chat IDs secret for Telegram notifier")
5463
}
5564
}
65+
if len(cids) == 0 {
66+
return errors.New("no chat IDs provided for Telegram notifier")
67+
}
68+
69+
parsedChatIDs, err := parseChatIDs(cids)
70+
if err != nil {
71+
return errors.Wrap(err, "cannot parse chat IDs for Telegram notifier")
72+
}
5673

5774
bot, err := gotgbot.NewBot(token, &gotgbot.BotOpts{
5875
BotClient: &gotgbot.BaseBotClient{
@@ -90,15 +107,67 @@ func (c *Client) Send(entry model.NotifEntry) error {
90107
return err
91108
}
92109

93-
for _, chatID := range chatIDs {
94-
_, err := bot.SendMessage(chatID, string(body), &gotgbot.SendMessageOpts{
95-
ParseMode: gotgbot.ParseModeMarkdown,
96-
LinkPreviewOptions: &gotgbot.LinkPreviewOptions{IsDisabled: true},
97-
})
98-
if err != nil {
99-
return err
110+
for _, cid := range parsedChatIDs {
111+
if len(cid.topics) > 0 {
112+
for _, topic := range cid.topics {
113+
if err = sendTelegramMessage(bot, cid.id, topic, string(body)); err != nil {
114+
return err
115+
}
116+
}
117+
} else {
118+
if err = sendTelegramMessage(bot, cid.id, 0, string(body)); err != nil {
119+
return err
120+
}
100121
}
101122
}
102123

103124
return nil
104125
}
126+
127+
func parseChatIDs(entries []interface{}) ([]chatID, error) {
128+
var chatIDs []chatID
129+
for _, entry := range entries {
130+
switch v := entry.(type) {
131+
case int:
132+
chatIDs = append(chatIDs, chatID{id: int64(v)})
133+
case int64:
134+
chatIDs = append(chatIDs, chatID{id: v})
135+
case string:
136+
parts := strings.Split(v, ":")
137+
if len(parts) < 1 || len(parts) > 2 {
138+
return nil, errors.Errorf("invalid chat ID %q", v)
139+
}
140+
id, err := strconv.ParseInt(parts[0], 10, 64)
141+
if err != nil {
142+
return nil, errors.Wrap(err, "invalid chat ID")
143+
}
144+
var topics []int64
145+
if len(parts) == 2 {
146+
topicParts := strings.Split(parts[1], ";")
147+
for _, topicPart := range topicParts {
148+
topic, err := strconv.ParseInt(topicPart, 10, 64)
149+
if err != nil {
150+
return nil, errors.Wrapf(err, "invalid topic %q for chat ID %d", topicPart, id)
151+
}
152+
topics = append(topics, topic)
153+
}
154+
}
155+
chatIDs = append(chatIDs, chatID{
156+
id: id,
157+
topics: topics,
158+
})
159+
default:
160+
return nil, errors.Errorf("invalid chat ID %v (type=%T)", entry, entry)
161+
}
162+
}
163+
return chatIDs, nil
164+
}
165+
166+
func sendTelegramMessage(bot *gotgbot.Bot, chatID int64, threadID int64, message string) error {
167+
_, err := bot.SendMessage(chatID, message, &gotgbot.SendMessageOpts{
168+
MessageThreadId: threadID,
169+
ParseMode: gotgbot.ParseModeMarkdown,
170+
LinkPreviewOptions: &gotgbot.LinkPreviewOptions{IsDisabled: true},
171+
})
172+
return err
173+
}
+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package telegram
2+
3+
import (
4+
"testing"
5+
6+
"github.com/pkg/errors"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestParseChatIDs(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
entries []interface{}
14+
expected []chatID
15+
err error
16+
}{
17+
{
18+
name: "valid integers",
19+
entries: []interface{}{8547439, 1234567},
20+
expected: []chatID{
21+
{id: 8547439},
22+
{id: 1234567},
23+
},
24+
err: nil,
25+
},
26+
{
27+
name: "valid strings with topics",
28+
entries: []interface{}{"567891234:25", "891256734:25;12"},
29+
expected: []chatID{
30+
{id: 567891234, topics: []int64{25}},
31+
{id: 891256734, topics: []int64{25, 12}},
32+
},
33+
err: nil,
34+
},
35+
{
36+
name: "invalid format",
37+
entries: []interface{}{"invalid_format"},
38+
expected: nil,
39+
err: errors.New(`invalid chat ID: strconv.ParseInt: parsing "invalid_format": invalid syntax`),
40+
},
41+
{
42+
name: "invalid type",
43+
entries: []interface{}{true},
44+
expected: nil,
45+
err: errors.New("invalid chat ID true (type=bool)"),
46+
},
47+
{
48+
name: "empty string",
49+
entries: []interface{}{""},
50+
expected: nil,
51+
err: errors.New(`invalid chat ID: strconv.ParseInt: parsing "": invalid syntax`),
52+
},
53+
{
54+
name: "string with invalid topic",
55+
entries: []interface{}{"567891234:invalid"},
56+
expected: nil,
57+
err: errors.New(`invalid topic "invalid" for chat ID 567891234: strconv.ParseInt: parsing "invalid": invalid syntax`),
58+
},
59+
{
60+
name: "mixed valid and invalid entries",
61+
entries: []interface{}{8547439, "567891234:25", "invalid_format", true},
62+
expected: nil,
63+
err: errors.New(`invalid chat ID: strconv.ParseInt: parsing "invalid_format": invalid syntax`),
64+
},
65+
{
66+
name: "invalid format with too many parts",
67+
entries: []interface{}{"567891234:25:extra"},
68+
expected: nil,
69+
err: errors.New(`invalid chat ID "567891234:25:extra"`),
70+
},
71+
}
72+
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) {
75+
res, err := parseChatIDs(tt.entries)
76+
if tt.err != nil {
77+
require.EqualError(t, err, tt.err.Error())
78+
} else {
79+
require.NoError(t, err)
80+
}
81+
require.Equal(t, tt.expected, res)
82+
})
83+
}
84+
}

0 commit comments

Comments
 (0)