mirror of
https://github.com/crazy-max/diun.git
synced 2025-12-25 06:49:28 +01:00
Merge pull request #1308 from crazy-max/telegram-topics
telegram: add topics support
This commit is contained in:
@@ -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`
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
84
internal/notif/telegram/client_test.go
Normal file
84
internal/notif/telegram/client_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user