diff --git a/CHANGELOG.md b/CHANGELOG.md index db497019e..52f8ee2d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Alerting -- +- [FEATURE] Telegram: allow configuring a custom Bot API base URL for local or proxied Telegram API servers. ## Scope Glossary @@ -43,4 +43,3 @@ Scopes must have an order to ensure consistency and ease of search, this helps u 3. `[BUGFIX]` 4. `[ENHANCEMENT]` 5. `[ADMIN]` - diff --git a/receivers/telegram/v1/api_url_test.go b/receivers/telegram/v1/api_url_test.go new file mode 100644 index 000000000..c117bf55c --- /dev/null +++ b/receivers/telegram/v1/api_url_test.go @@ -0,0 +1,74 @@ +package v1 + +import ( + "encoding/json" + "mime/multipart" + "testing" + + "github.com/stretchr/testify/require" + + receiversTesting "github.com/grafana/alerting/receivers/testing" +) + +func TestNewConfigAPIURL(t *testing.T) { + t.Run("accepts and normalizes an absolute API URL", func(t *testing.T) { + cfg, err := NewConfig( + json.RawMessage(`{"bottoken":"test-token","chatid":"12345678","api_url":"http://localhost:8081/telegram/"}`), + receiversTesting.DecryptForTesting(nil), + ) + + require.NoError(t, err) + require.Equal(t, "http://localhost:8081/telegram", cfg.APIURL) + }) + + t.Run("rejects a relative API URL", func(t *testing.T) { + _, err := NewConfig( + json.RawMessage(`{"bottoken":"test-token","chatid":"12345678","api_url":"telegram.local"}`), + receiversTesting.DecryptForTesting(nil), + ) + + require.ErrorContains(t, err, "invalid Telegram API URL") + }) + + t.Run("rejects a non-HTTP API URL", func(t *testing.T) { + _, err := NewConfig( + json.RawMessage(`{"bottoken":"test-token","chatid":"12345678","api_url":"ftp://telegram.local"}`), + receiversTesting.DecryptForTesting(nil), + ) + + require.ErrorContains(t, err, "invalid Telegram API URL") + }) +} + +func TestNewWebhookSyncCmdAPIURL(t *testing.T) { + noop := func(*multipart.Writer) error { return nil } + + t.Run("uses the configured API URL", func(t *testing.T) { + n := &Notifier{settings: Config{ + APIURL: "http://localhost:8081/telegram", + BotToken: "test-token", + ChatID: "12345678", + }} + + cmd, err := n.newWebhookSyncCmd("sendMessage", noop) + + require.NoError(t, err) + require.Equal(t, "http://localhost:8081/telegram/bottest-token/sendMessage", cmd.URL) + }) + + t.Run("keeps the default integration-test override when API URL is unset", func(t *testing.T) { + previousAPIURL := APIURL + APIURL = "http://telegram.test/bot%s/%s" + t.Cleanup(func() { APIURL = previousAPIURL }) + + n := &Notifier{settings: Config{ + BotToken: "test-token", + ChatID: "12345678", + }} + + cmd, err := n.newWebhookSyncCmd("sendPhoto", noop) + + require.NoError(t, err) + require.Equal(t, "http://telegram.test/bottest-token/sendPhoto", cmd.URL) + }) +} diff --git a/receivers/telegram/v1/config.go b/receivers/telegram/v1/config.go index ec9d2028c..620e30486 100644 --- a/receivers/telegram/v1/config.go +++ b/receivers/telegram/v1/config.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "net/url" "strconv" "strings" @@ -21,6 +22,7 @@ const DefaultTelegramParseMode = "HTML" var SupportedParseMode = map[string]string{"Markdown": "Markdown", "MarkdownV2": "MarkdownV2", DefaultTelegramParseMode: "HTML", "None": ""} type Config struct { + APIURL string `json:"api_url,omitempty" yaml:"api_url,omitempty"` BotToken string `json:"bottoken,omitempty" yaml:"bottoken,omitempty"` ChatID string `json:"chatid,omitempty" yaml:"chatid,omitempty"` MessageThreadID string `json:"message_thread_id,omitempty" yaml:"message_thread_id,omitempty"` @@ -33,6 +35,7 @@ type Config struct { func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Config, error) { raw := struct { + APIURL string `json:"api_url,omitempty" yaml:"api_url,omitempty"` BotToken string `json:"bottoken,omitempty" yaml:"bottoken,omitempty"` ChatID receivers.OptionalNumber `json:"chatid,omitempty" yaml:"chatid,omitempty"` MessageThreadID receivers.OptionalNumber `json:"message_thread_id,omitempty" yaml:"message_thread_id,omitempty"` @@ -47,6 +50,7 @@ func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Confi } settings := Config{ + APIURL: raw.APIURL, Message: raw.Message, ParseMode: raw.ParseMode, DisableWebPagePreview: raw.DisableWebPagePreview, @@ -64,6 +68,14 @@ func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Confi return settings, errors.New("could not find Chat Id in settings") } + if settings.APIURL != "" { + u, err := url.Parse(settings.APIURL) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" || u.RawQuery != "" || u.Fragment != "" { + return settings, fmt.Errorf("invalid Telegram API URL %q", settings.APIURL) + } + settings.APIURL = strings.TrimRight(u.String(), "/") + } + if settings.Message == "" { settings.Message = templates.DefaultMessageEmbed } @@ -117,6 +129,15 @@ var Schema = schema.NewIntegrationSchemaVersion(schema.IntegrationSchemaVersion{ Version: Version, CanCreate: true, Options: []schema.Field{ + { + Label: "API URL", + Element: schema.ElementTypeInput, + InputType: schema.InputTypeText, + Placeholder: "https://api.telegram.org", + Description: "Telegram Bot API base URL. Leave empty to use the default Telegram API.", + PropertyName: "api_url", + Protected: true, + }, { Label: "BOT API Token", Element: schema.ElementTypeInput, diff --git a/receivers/telegram/v1/config_test.go b/receivers/telegram/v1/config_test.go index 77db21c33..ae971eaa9 100644 --- a/receivers/telegram/v1/config_test.go +++ b/receivers/telegram/v1/config_test.go @@ -121,6 +121,7 @@ func TestNewConfig(t *testing.T) { name: "Extracts all fields", settings: FullValidConfigForTesting, expectedConfig: Config{ + APIURL: "http://localhost:8081/telegram", BotToken: "test-token", ChatID: "12345678", Message: "test-message", @@ -136,6 +137,7 @@ func TestNewConfig(t *testing.T) { settings: FullValidConfigForTesting, secureSettings: receiversTesting.ReadSecretsJSONForTesting(FullValidSecretsForTesting), expectedConfig: Config{ + APIURL: "http://localhost:8081/telegram", BotToken: "test-secret-token", ChatID: "12345678", Message: "test-message", diff --git a/receivers/telegram/v1/telegram.go b/receivers/telegram/v1/telegram.go index 5709c8788..b535be29b 100644 --- a/receivers/telegram/v1/telegram.go +++ b/receivers/telegram/v1/telegram.go @@ -5,13 +5,13 @@ import ( "context" "fmt" "mime/multipart" + "strings" + "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/types" - "github.com/go-kit/log" - "github.com/grafana/alerting/images" "github.com/grafana/alerting/receivers" "github.com/grafana/alerting/templates" @@ -173,8 +173,13 @@ func (tn *Notifier) newWebhookSyncCmd(action string, fn func(writer *multipart.W return nil, fmt.Errorf("failed to close multipart: %w", err) } + requestURL := fmt.Sprintf(APIURL, tn.settings.BotToken, action) + if tn.settings.APIURL != "" { + requestURL = fmt.Sprintf("%s/bot%s/%s", strings.TrimRight(tn.settings.APIURL, "/"), tn.settings.BotToken, action) + } + cmd := &receivers.SendWebhookSettings{ - URL: fmt.Sprintf(APIURL, tn.settings.BotToken, action), + URL: requestURL, Body: b.String(), HTTPMethod: "POST", HTTPHeader: map[string]string{ diff --git a/receivers/telegram/v1/testing.go b/receivers/telegram/v1/testing.go index b65c02ee6..7735737e7 100644 --- a/receivers/telegram/v1/testing.go +++ b/receivers/telegram/v1/testing.go @@ -2,6 +2,7 @@ package v1 // FullValidConfigForTesting is a string representation of a JSON object that contains all fields supported by the notifier Config. It can be used without secrets. const FullValidConfigForTesting = `{ + "api_url" :"http://localhost:8081/telegram", "bottoken" :"test-token", "chatid" :"12345678", "message" :"test-message",