Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Alerting

-
- [FEATURE] Telegram: allow configuring a custom Bot API base URL for local or proxied Telegram API servers.

## Scope Glossary

Expand Down Expand Up @@ -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]`

74 changes: 74 additions & 0 deletions receivers/telegram/v1/api_url_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
21 changes: 21 additions & 0 deletions receivers/telegram/v1/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strconv"
"strings"

Expand All @@ -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"`
Expand All @@ -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"`
Expand All @@ -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,
Expand All @@ -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
}
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions receivers/telegram/v1/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
11 changes: 8 additions & 3 deletions receivers/telegram/v1/telegram.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{
Expand Down
1 change: 1 addition & 0 deletions receivers/telegram/v1/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down