From cc279e6057c032eb3d1eb067a20f0345e2d69643 Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Sun, 16 Aug 2026 13:53:27 -0300 Subject: [PATCH] Support telegram notifications --- .../service/config/settings-alerting.go | 4 + shared/services/config/alertmanager-config.go | 43 +++++++++ .../config/alertmanager-config_test.go | 90 +++++++++++++++++++ .../assets/install/alerting/alertmanager.tmpl | 13 +++ 4 files changed, 150 insertions(+) create mode 100644 shared/services/config/alertmanager-config_test.go diff --git a/rocketpool-cli/service/config/settings-alerting.go b/rocketpool-cli/service/config/settings-alerting.go index b15c5f7b3..55d9b5468 100644 --- a/rocketpool-cli/service/config/settings-alerting.go +++ b/rocketpool-cli/service/config/settings-alerting.go @@ -14,6 +14,8 @@ var alertingParametersNativeMode = map[string]interface{}{ "nativeModeHost": nil, "nativeModePort": nil, "discordWebhookURL": nil, + "telegramBotToken": nil, + "telegramChatId": nil, "pushoverToken": nil, "pushoverUserKey": nil, "alertEnabled_FeeRecipientChanged": nil, @@ -36,6 +38,8 @@ var alertingParametersDockerMode = map[string]interface{}{ "openPort": nil, "containerTag": nil, "discordWebhookURL": nil, + "telegramBotToken": nil, + "telegramChatId": nil, "pushoverToken": nil, "pushoverUserKey": nil, "alertEnabled_ClientSyncStatusBeacon": nil, diff --git a/shared/services/config/alertmanager-config.go b/shared/services/config/alertmanager-config.go index 1a5172e8e..45efb4602 100644 --- a/shared/services/config/alertmanager-config.go +++ b/shared/services/config/alertmanager-config.go @@ -2,6 +2,8 @@ package config import ( "fmt" + "strconv" + "strings" "github.com/mitchellh/go-homedir" "golang.org/x/text/cases" @@ -57,6 +59,11 @@ type AlertmanagerConfig struct { // The Discord webhook URL for alert notifications DiscordWebhookURL config.Parameter `yaml:"discordWebhookURL,omitempty"` + // The Telegram bot token for alert notifications + TelegramBotToken config.Parameter `yaml:"telegramBotToken,omitempty"` + // The Telegram chat ID for alert notifications + TelegramChatID config.Parameter `yaml:"telegramChatId,omitempty"` + // The Pushover Token for alert notifications PushoverToken config.Parameter `yaml:"pushoverToken,omitempty"` // The Pushover User Key for alert notifications @@ -185,6 +192,28 @@ func NewAlertmanagerConfig(cfg *RocketPoolConfig) *AlertmanagerConfig { OverwriteOnUpgrade: false, }, + TelegramBotToken: config.Parameter{ + ID: "telegramBotToken", + Name: "Alertmanager Telegram Bot Token", + Description: "Telegram notifications are sent via the Telegram Bot API. Create a bot with @BotFather (https://t.me/BotFather) and paste the token here (the token only — do not include a 'bot' prefix). Both this token and a chat ID are required; filling only one does nothing.", + Type: config.ParameterType_String, + Default: map[config.Network]interface{}{config.Network_All: ""}, + AffectsContainers: []config.ContainerID{config.ContainerID_Alertmanager}, + CanBeBlank: true, + OverwriteOnUpgrade: false, + }, + + TelegramChatID: config.Parameter{ + ID: "telegramChatId", + Name: "Alertmanager Telegram Chat ID", + Description: "Numeric Telegram chat ID to send alerts to (user, group, or channel). Group and channel IDs are negative (typically -100…). For DMs, send /start to your bot first. For groups or channels, add the bot as a member (admin for channels). Both this chat ID and a bot token are required; filling only one does nothing.", + Type: config.ParameterType_String, + Default: map[config.Network]interface{}{config.Network_All: ""}, + AffectsContainers: []config.ContainerID{config.ContainerID_Alertmanager}, + CanBeBlank: true, + OverwriteOnUpgrade: false, + }, + PushoverToken: config.Parameter{ ID: "pushoverToken", Name: "Alertmanager Pushover Token", @@ -328,6 +357,8 @@ func (cfg *AlertmanagerConfig) GetParameters() []*config.Parameter { &cfg.NativeModeHost, &cfg.NativeModePort, &cfg.DiscordWebhookURL, + &cfg.TelegramBotToken, + &cfg.TelegramChatID, &cfg.PushoverToken, &cfg.PushoverUserKey, &cfg.ContainerTag, @@ -359,6 +390,18 @@ func (cfg *AlertmanagerConfig) GetConfigTitle() string { return cfg.Title } +// TelegramEnabled reports whether Telegram notifications should be rendered into +// alertmanager.yml. Both a bot token and a numeric chat ID are required. +func (cfg *AlertmanagerConfig) TelegramEnabled() bool { + token, _ := cfg.TelegramBotToken.Value.(string) + chat, _ := cfg.TelegramChatID.Value.(string) + if strings.TrimSpace(token) == "" || strings.TrimSpace(chat) == "" { + return false + } + _, err := strconv.ParseInt(strings.TrimSpace(chat), 10, 64) + return err == nil +} + // Used by text/template to format alertmanager.yml func (cfg *AlertmanagerConfig) GetOpenPorts() string { portMode := cfg.OpenPort.Value.(config.RPCMode) diff --git a/shared/services/config/alertmanager-config_test.go b/shared/services/config/alertmanager-config_test.go new file mode 100644 index 000000000..365bee3ea --- /dev/null +++ b/shared/services/config/alertmanager-config_test.go @@ -0,0 +1,90 @@ +package config + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + "text/template" +) + +func TestTelegramEnabled(t *testing.T) { + tests := []struct { + name string + token string + chat string + want bool + }{ + {name: "both empty", want: false}, + {name: "empty token", token: "", chat: "12345", want: false}, + {name: "empty chat", token: "123:ABC", chat: "", want: false}, + {name: "whitespace only", token: " ", chat: " ", want: false}, + {name: "non-numeric chat", token: "123:ABC", chat: "not-a-chat-id", want: false}, + {name: "username chat", token: "123:ABC", chat: "@mychannel", want: false}, + {name: "valid positive chat", token: "123:ABC", chat: "12345", want: true}, + {name: "valid negative group chat", token: "123:ABC", chat: "-1001234567890", want: true}, + {name: "padded numeric chat", token: " 123:ABC ", chat: " 12345 ", want: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := &AlertmanagerConfig{} + cfg.TelegramBotToken.Value = tt.token + cfg.TelegramChatID.Value = tt.chat + if got := cfg.TelegramEnabled(); got != tt.want { + t.Fatalf("TelegramEnabled() = %v, want %v (token=%q chat=%q)", got, tt.want, tt.token, tt.chat) + } + }) + } +} + +func TestAlertmanagerTemplateTelegramConfigs(t *testing.T) { + tmplPath := filepath.Join("..", "rocketpool", "assets", "install", "alerting", "alertmanager.tmpl") + src, err := os.ReadFile(tmplPath) + if err != nil { + t.Fatalf("read alertmanager template: %v", err) + } + + tmpl, err := template.New("alertmanager.tmpl").Parse(string(src)) + if err != nil { + t.Fatalf("parse alertmanager template: %v", err) + } + + cfg := &AlertmanagerConfig{} + cfg.TelegramBotToken.Value = "123:ABC" + cfg.TelegramChatID.Value = "-1001234567890" + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, cfg); err != nil { + t.Fatalf("execute alertmanager template: %v", err) + } + out := buf.String() + + if !strings.Contains(out, "telegram_configs:") { + t.Fatalf("expected telegram_configs when Telegram is enabled:\n%s", out) + } + if !strings.Contains(out, `bot_token: "123:ABC"`) { + t.Fatalf("expected quoted bot token:\n%s", out) + } + if !strings.Contains(out, "chat_id: -1001234567890") { + t.Fatalf("expected unquoted chat ID:\n%s", out) + } + if strings.Count(out, "telegram_configs:") != 2 { + t.Fatalf("expected telegram_configs on both receivers, found %d:\n%s", strings.Count(out, "telegram_configs:"), out) + } + if strings.Count(out, "send_resolved: false") < 1 { + t.Fatalf("expected send_resolved: false on the info receiver:\n%s", out) + } + + disabled := &AlertmanagerConfig{} + disabled.TelegramBotToken.Value = "123:ABC" + disabled.TelegramChatID.Value = "not-a-chat-id" + buf.Reset() + if err := tmpl.Execute(&buf, disabled); err != nil { + t.Fatalf("execute alertmanager template with invalid chat ID: %v", err) + } + if strings.Contains(buf.String(), "telegram_configs:") { + t.Fatalf("did not expect telegram_configs for invalid chat ID:\n%s", buf.String()) + } +} diff --git a/shared/services/rocketpool/assets/install/alerting/alertmanager.tmpl b/shared/services/rocketpool/assets/install/alerting/alertmanager.tmpl index 6eafe6e8b..74d13caae 100644 --- a/shared/services/rocketpool/assets/install/alerting/alertmanager.tmpl +++ b/shared/services/rocketpool/assets/install/alerting/alertmanager.tmpl @@ -41,6 +41,12 @@ receivers: - webhook_url: "{{ .DiscordWebhookURL.Value }}" {{- end }} + {{- if .TelegramEnabled }} + telegram_configs: + - bot_token: "{{ .TelegramBotToken.Value }}" + chat_id: {{ .TelegramChatID.Value }} + {{- end }} + {{- if and .PushoverToken.Value .PushoverUserKey.Value }} pushover_configs: - token: "{{ .PushoverToken.Value }}" @@ -54,6 +60,13 @@ receivers: - webhook_url: "{{ .DiscordWebhookURL.Value }}" send_resolved: false {{- end }} + + {{- if .TelegramEnabled }} + telegram_configs: + - bot_token: "{{ .TelegramBotToken.Value }}" + chat_id: {{ .TelegramChatID.Value }} + send_resolved: false + {{- end }} {{- if and .PushoverToken.Value .PushoverUserKey.Value }} pushover_configs: