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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
13 changes: 10 additions & 3 deletions pkg/providers/telegram/telegram.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"`
}
Expand All @@ -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)
Expand Down
104 changes: 104 additions & 0 deletions pkg/providers/telegram/telegram_test.go
Original file line number Diff line number Diff line change
@@ -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")
})
}
}
Loading