diff --git a/README.md b/README.md index 9260952..e483769 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ telegram: - id: "tel" telegram_api_key: "XXXXXXXXXXXX" telegram_chat_id: "XXXXXXXX" + telegram_thread_id: "YYYYYYYYYY" # Optional: The message thread ID to send messages to (https://www.shoutrrr.com/services/telegram/#message-threads) telegram_format: "{{data}}" telegram_parsemode: "Markdown" # None/Markdown/MarkdownV2/HTML (https://core.telegram.org/bots/api#formatting-options) diff --git a/go.mod b/go.mod index 84c2724..976a111 100644 --- a/go.mod +++ b/go.mod @@ -7,12 +7,12 @@ require ( github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/containrrr/shoutrrr v0.8.0 github.com/json-iterator/go v1.1.12 - github.com/logrusorgru/aurora v2.0.3+incompatible github.com/oriser/regroup v0.0.0-20210730155327-fca8d7531263 github.com/pkg/errors v0.9.1 github.com/projectdiscovery/goflags v0.1.74 github.com/projectdiscovery/gologger v1.1.68 github.com/projectdiscovery/utils v0.9.0 + github.com/stretchr/testify v1.11.1 go.uber.org/multierr v1.11.0 go.uber.org/ratelimit v0.3.0 gopkg.in/yaml.v3 v3.0.1 @@ -39,6 +39,7 @@ require ( github.com/charmbracelet/x/ansi v0.3.2 // indirect github.com/cheggaaa/pb/v3 v3.1.4 // indirect github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/djherbis/times v1.6.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect @@ -54,6 +55,7 @@ require ( github.com/imdario/mergo v0.3.15 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/klauspost/pgzip v1.2.6 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/logrusorgru/aurora/v4 v4.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect @@ -74,6 +76,7 @@ require ( github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/nwaples/rardecode/v2 v2.2.2 // indirect github.com/pierrec/lz4/v4 v4.1.23 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/projectdiscovery/blackrock v0.0.1 // indirect github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582 // indirect diff --git a/pkg/providers/telegram/telegram.go b/pkg/providers/telegram/telegram.go index 54edba9..fa60b9b 100644 --- a/pkg/providers/telegram/telegram.go +++ b/pkg/providers/telegram/telegram.go @@ -6,12 +6,14 @@ import ( "github.com/containrrr/shoutrrr" "github.com/pkg/errors" "go.uber.org/multierr" - "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/notify/pkg/utils" sliceutil "github.com/projectdiscovery/utils/slice" ) +// shoutrrrSendFunc is a package-level variable to allow mocking in tests. +var shoutrrrSendFunc = shoutrrr.Send + type Provider struct { Telegram []*Options `yaml:"telegram,omitempty"` counter int @@ -21,6 +23,7 @@ type Options struct { ID string `yaml:"id,omitempty"` TelegramAPIKey string `yaml:"telegram_api_key,omitempty"` TelegramChatID string `yaml:"telegram_chat_id,omitempty"` + TelegramThreadID string `yaml:"telegram_thread_id,omitempty"` TelegramFormat string `yaml:"telegram_format,omitempty"` TelegramParseMode string `yaml:"telegram_parsemode,omitempty"` } @@ -47,8 +50,12 @@ func (p *Provider) Send(message, CliFormat string) error { if pr.TelegramParseMode == "" { pr.TelegramParseMode = "None" } - url := fmt.Sprintf("telegram://%s@telegram?channels=%s&parsemode=%s", pr.TelegramAPIKey, pr.TelegramChatID, pr.TelegramParseMode) - err := shoutrrr.Send(url, msg) + telegramChatID := pr.TelegramChatID + if pr.TelegramThreadID != "" { + telegramChatID = fmt.Sprintf("%s:%s", pr.TelegramChatID, pr.TelegramThreadID) + } + url := fmt.Sprintf("telegram://%s@telegram?channels=%s&parsemode=%s", pr.TelegramAPIKey, telegramChatID, pr.TelegramParseMode) + err := shoutrrrSendFunc(url, msg) if err != nil { err = errors.Wrap(err, fmt.Sprintf("failed to send telegram notification for id: %s ", pr.ID)) TelegramErr = multierr.Append(TelegramErr, err) diff --git a/pkg/providers/telegram/telegram_test.go b/pkg/providers/telegram/telegram_test.go new file mode 100644 index 0000000..db31c92 --- /dev/null +++ b/pkg/providers/telegram/telegram_test.go @@ -0,0 +1,104 @@ +package telegram + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +// capturedURL will store the URL passed to the mocked shoutrrrSendFunc +var capturedURL string + +// TestTelegramSendURLWithThreadID tests the Send method of the Telegram provider, +// focusing on how the URL is constructed with and without a thread ID. +func TestTelegramSendURLWithThreadID(t *testing.T) { + // Store the original shoutrrrSendFunc and defer its restoration + originalSendFunc := shoutrrrSendFunc + defer func() { + shoutrrrSendFunc = originalSendFunc + }() + + // Mock shoutrrrSendFunc to capture the URL and avoid actual sending + shoutrrrSendFunc = func(serviceURL string, message string) error { + capturedURL = serviceURL + return nil // Simulate success + } + + tests := []struct { + name string + options Options + expectedChatIDInURL string + }{ + { + name: "with thread_id", + options: Options{ + ID: "test-with-thread", + TelegramAPIKey: "testAPIKey", + TelegramChatID: "testChatID", + TelegramThreadID: "testThreadID", + }, + expectedChatIDInURL: "testChatID:testThreadID", + }, + { + name: "without thread_id", + options: Options{ + ID: "test-without-thread", + TelegramAPIKey: "testAPIKey2", + TelegramChatID: "testChatID2", + }, + expectedChatIDInURL: "testChatID2", + }, + { + name: "with thread_id but empty", + options: Options{ + ID: "test-with-empty-thread", + TelegramAPIKey: "testAPIKey3", + TelegramChatID: "testChatID3", + TelegramThreadID: "", // Explicitly empty + }, + expectedChatIDInURL: "testChatID3", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + capturedURL = "" // Reset captured URL for each test run + provider, err := New([]*Options{&tt.options}, nil) + require.NoError(t, err, "New() should not return an error") + require.NotNil(t, provider, "New() should return a provider") + require.Len(t, provider.Telegram, 1, "Provider should have one Telegram option") + + err = provider.Send("test message", "") + require.NoError(t, err, "Send() should not return an error") + + parsedURL, err := url.Parse(capturedURL) + require.NoError(t, err, "Captured URL should be parseable") + + channels := parsedURL.Query().Get("channels") + require.Equal(t, tt.expectedChatIDInURL, channels, "Chat ID in URL does not match expected") + + // Verify other parts of the URL + expectedScheme := "telegram" + require.Equal(t, expectedScheme, parsedURL.Scheme, "URL scheme does not match") + + // Check API Key (Username part of Userinfo) + expectedAPIKey := tt.options.TelegramAPIKey + require.NotNil(t, parsedURL.User, "URL Userinfo should not be nil") + actualAPIKey := parsedURL.User.Username() + require.Equal(t, expectedAPIKey, actualAPIKey, "URL API key (username) does not match") + + // Check Host part + expectedHost := "telegram" + require.Equal(t, expectedHost, parsedURL.Host, "URL host does not match") + + parseMode := parsedURL.Query().Get("parsemode") + // Default ParseMode is "None" if not specified in options + expectedParseMode := "None" + if tt.options.TelegramParseMode != "" { + expectedParseMode = tt.options.TelegramParseMode + } + require.Equal(t, expectedParseMode, parseMode, "URL parsemode does not match") + }) + } +}