Merge pull request #1308 from crazy-max/telegram-topics

telegram: add topics support
This commit is contained in:
CrazyMax
2024-12-19 00:59:55 +01:00
committed by GitHub
7 changed files with 207 additions and 28 deletions

View File

@@ -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,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 |
@@ -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`

View File

@@ -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{
@@ -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,
},
},

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -3,6 +3,7 @@ package telegram
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"text/template"
@@ -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{
@@ -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{
@@ -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
}

View File

@@ -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)
})
}
}