Repository: nikoksr/notify

Issue:
feat(service): proxy setting
Services like discord, telegram are not available directly in my country, so proxy setup is required for me.

In telegram, I can use `SetClient` to set proxy, but how can I set proxy for discord or other service
```go
client, err := api.NewBotAPIWithClient(apiToken, proxy.ProxyHttpClient)
if err != nil {
	return nil, err
}
t := &telegram.Telegram{}
t.SetClient(client)
```


Discussion:

Solution patch:
diff --git a/service/discord/README.md b/service/discord/README.md
new file mode 100644
index 0000000..048172b
--- /dev/null
+++ b/service/discord/README.md
@@ -0,0 +1,122 @@
+# Discord
+
+[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/nikoksr/notify/service/discord)
+
+## Prerequisites
+
+To use the Discord notification service, you will need:
+
+- A Discord Bot Token or OAuth2 Token
+- Channel IDs where you want to send messages
+
+You can create a Discord bot and get your token from the [Discord Developer Portal](https://discord.com/developers/applications).
+
+## Usage
+
+### Basic Usage
+
+```go
+package main
+
+import (
+	"context"
+	"log"
+
+	"github.com/nikoksr/notify"
+	"github.com/nikoksr/notify/service/discord"
+)
+
+func main() {
+	discordSvc := discord.New()
+
+	// Authenticate with your bot token
+	if err := discordSvc.AuthenticateWithBotToken("your-bot-token"); err != nil {
+		log.Fatalf("failed to authenticate: %s", err.Error())
+	}
+
+	// Add channel IDs to send messages to
+	discordSvc.AddReceivers("channel-id-1", "channel-id-2")
+
+	notifier := notify.New()
+	notifier.UseServices(discordSvc)
+
+	if err := notifier.Send(context.Background(), "Subject", "Message body"); err != nil {
+		log.Fatalf("failed to send notification: %s", err.Error())
+	}
+
+	log.Println("notification sent")
+}
+```
+
+### OAuth2 Authentication
+
+You can also authenticate using an OAuth2 token:
+
+```go
+discordSvc := discord.New()
+
+if err := discordSvc.AuthenticateWithOAuth2Token("your-oauth2-token"); err != nil {
+	log.Fatalf("failed to authenticate: %s", err.Error())
+}
+```
+
+### Advanced Configuration
+
+For advanced use cases such as proxy configuration or custom HTTP client settings, you can use `SetClient` with `DefaultSession`:
+
+```go
+package main
+
+import (
+	"context"
+	"log"
+	"net/http"
+	"net/url"
+
+	"github.com/nikoksr/notify"
+	"github.com/nikoksr/notify/service/discord"
+)
+
+func main() {
+	// Create a custom HTTP client with proxy
+	proxyURL, _ := url.Parse("http://proxy.example.com:8080")
+	httpClient := &http.Client{
+		Transport: &http.Transport{
+			Proxy: http.ProxyURL(proxyURL),
+		},
+	}
+
+	// Get a Discord session with default configuration
+	session := discord.DefaultSession()
+	session.Client = httpClient
+
+	// Create Discord service and set the custom session
+	discordSvc := discord.New()
+	discordSvc.SetClient(session)
+
+	// Authenticate - this will preserve the custom HTTP client
+	if err := discordSvc.AuthenticateWithBotToken("your-bot-token"); err != nil {
+		log.Fatalf("failed to authenticate: %s", err.Error())
+	}
+
+	discordSvc.AddReceivers("channel-id")
+
+	notifier := notify.New()
+	notifier.UseServices(discordSvc)
+
+	if err := notifier.Send(context.Background(), "Subject", "Message via proxy"); err != nil {
+		log.Fatalf("failed to send notification: %s", err.Error())
+	}
+
+	log.Println("notification sent")
+}
+```
+
+The `SetClient` method allows full control over the Discord session configuration, including:
+
+- Custom HTTP client (for proxies, custom timeouts, etc.)
+- Shard configuration
+- Connection settings
+- Rate limiting behavior
+
+Note that `DefaultSession()` allows you to configure the Discord session without importing the `discordgo` package directly.
diff --git a/service/discord/discord.go b/service/discord/discord.go
index eac4607..718638b 100644
--- a/service/discord/discord.go
+++ b/service/discord/discord.go
@@ -22,6 +22,9 @@ type Discord struct {
 }
 
 // New returns a new instance of a Discord notification service.
+// The instance is created with a default Discord session. For advanced configuration
+// such as proxy support or custom timeouts, use SetClient to provide a custom session
+// before calling the authentication methods.
 func New() *Discord {
 	return &Discord{
 		client:     &discordgo.Session{},
@@ -29,8 +32,21 @@ func New() *Discord {
 	}
 }
 
-// authenticate will try and authenticate to discord.
+// authenticate will configure authentication on the existing Discord session.
+// If a custom session was set via SetClient, it preserves all custom configuration
+// (HTTP client, proxy settings, etc.) while setting the authentication token.
 func (d *Discord) authenticate(token string) error {
+	// If we have an existing session, configure it in-place
+	if d.client != nil {
+		if session, ok := d.client.(*discordgo.Session); ok {
+			session.Token = token
+			session.Identify.Token = token
+			session.Identify.Intents = discordgo.IntentsGuildMessageTyping
+			return nil
+		}
+	}
+
+	// Fallback: create new session (only if client is nil or not a Session)
 	client, err := discordgo.New(token)
 	if err != nil {
 		return err
@@ -43,6 +59,49 @@ func (d *Discord) authenticate(token string) error {
 	return nil
 }
 
+// DefaultSession returns a new Discord session with default configuration.
+// This allows configuring a custom session without importing the discordgo package.
+// The session is created with Discord's standard defaults and can be customized before
+// passing it to SetClient.
+//
+// Example - Configure proxy without importing discordgo:
+//
+//	session := discord.DefaultSession()
+//	session.Client = &http.Client{
+//	    Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
+//	}
+//	d := discord.New()
+//	d.SetClient(session)
+//	d.AuthenticateWithBotToken(token)
+func DefaultSession() *discordgo.Session {
+	// Use discordgo.New with empty token to get proper defaults
+	session, _ := discordgo.New("")
+	return session
+}
+
+// SetClient sets a custom Discord session, allowing full control over the
+// session configuration including HTTP client, proxy settings, intents, and
+// other Discord session options.
+//
+// This is useful for advanced use cases like proxy configuration or custom timeouts.
+// After calling SetClient, you can still use the AuthenticateWith* methods to set
+// the authentication token while preserving your custom configuration.
+//
+// Use DefaultSession() to get a session instance without importing discordgo.
+//
+// Example - Configure proxy, then authenticate:
+//
+//	session := discord.DefaultSession()
+//	session.Client = &http.Client{
+//	    Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
+//	}
+//	d := discord.New()
+//	d.SetClient(session)
+//	d.AuthenticateWithBotToken(token)  // Preserves proxy configuration
+func (d *Discord) SetClient(client *discordgo.Session) {
+	d.client = client
+}
+
 // AuthenticateWithBotToken authenticates you as a bot to Discord via the given access token.
 // For more info, see here: https://pkg.go.dev/github.com/bwmarrin/discordgo@v0.22.1#New
 func (d *Discord) AuthenticateWithBotToken(token string) error {



Test patch:
diff --git a/service/discord/discord_test.go b/service/discord/discord_test.go
index f74b825..df0c3ca 100644
--- a/service/discord/discord_test.go
+++ b/service/discord/discord_test.go
@@ -3,7 +3,9 @@ package discord
 import (
 	"context"
 	"errors"
+	"net/http"
 	"testing"
+	"time"
 
 	"github.com/bwmarrin/discordgo"
 	"github.com/stretchr/testify/require"
@@ -81,3 +83,204 @@ func TestDiscord_Send(t *testing.T) {
 		})
 	}
 }
+
+func TestDefaultSession(t *testing.T) {
+	t.Parallel()
+
+	session := DefaultSession()
+
+	// Verify it returns a non-nil session
+	require.NotNil(t, session, "DefaultSession should return a non-nil session")
+
+	// Verify critical defaults are set (these come from discordgo.New)
+	require.NotNil(t, session.Client, "Client should not be nil")
+	require.NotNil(t, session.Ratelimiter, "Ratelimiter should not be nil")
+	require.Equal(t, 3, session.MaxRestRetries, "MaxRestRetries should be 3")
+	require.True(t, session.ShouldRetryOnRateLimit, "ShouldRetryOnRateLimit should be true")
+	require.True(t, session.ShouldReconnectOnError, "ShouldReconnectOnError should be true")
+	require.Equal(t, 1, session.ShardCount, "ShardCount should be 1")
+	require.True(t, session.Compress, "Compress should be true")
+}
+
+func TestDiscord_SetClient(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name          string
+		setupSession  func() *discordgo.Session
+		setupDiscord  func() *Discord
+		validateAfter func(*testing.T, *Discord)
+	}{
+		{
+			name: "Set custom session before authentication",
+			setupSession: func() *discordgo.Session {
+				session := DefaultSession()
+				session.ShardID = 5
+				session.MaxRestRetries = 10
+				return session
+			},
+			setupDiscord: New,
+			validateAfter: func(t *testing.T, d *Discord) {
+				session, ok := d.client.(*discordgo.Session)
+				require.True(t, ok, "client should be a *discordgo.Session")
+				require.Equal(t, 5, session.ShardID, "ShardID should be preserved")
+				require.Equal(t, 10, session.MaxRestRetries, "MaxRestRetries should be preserved")
+			},
+		},
+		{
+			name: "Set custom session with custom HTTP client",
+			setupSession: func() *discordgo.Session {
+				session := DefaultSession()
+				session.Client = &http.Client{Timeout: 30 * time.Second}
+				return session
+			},
+			setupDiscord: New,
+			validateAfter: func(t *testing.T, d *Discord) {
+				session, ok := d.client.(*discordgo.Session)
+				require.True(t, ok, "client should be a *discordgo.Session")
+				require.NotNil(t, session.Client, "HTTP client should not be nil")
+				require.Equal(t, 30*time.Second, session.Client.Timeout, "Custom timeout should be preserved")
+			},
+		},
+		{
+			name:         "Replace existing session",
+			setupSession: DefaultSession,
+			setupDiscord: func() *Discord {
+				d := New()
+				// Set initial custom client
+				initialSession := DefaultSession()
+				initialSession.ShardID = 1
+				d.SetClient(initialSession)
+				return d
+			},
+			validateAfter: func(t *testing.T, d *Discord) {
+				session, ok := d.client.(*discordgo.Session)
+				require.True(t, ok, "client should be a *discordgo.Session")
+				// ShardID should be default (0) from new session, not 1 from initial
+				require.Equal(t, 0, session.ShardID, "ShardID should be from new session")
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			session := tt.setupSession()
+			d := tt.setupDiscord()
+
+			d.SetClient(session)
+
+			tt.validateAfter(t, d)
+		})
+	}
+}
+
+func TestDiscord_AuthenticatePreservesCustomSession(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name             string
+		setupSession     func() *discordgo.Session
+		authMethod       func(*Discord) error
+		expectedToken    string
+		validatePreserve func(*testing.T, *discordgo.Session)
+	}{
+		{
+			name: "AuthenticateWithBotToken preserves custom HTTP client",
+			setupSession: func() *discordgo.Session {
+				session := DefaultSession()
+				session.Client = &http.Client{Timeout: 45 * time.Second}
+				return session
+			},
+			authMethod: func(d *Discord) error {
+				return d.AuthenticateWithBotToken("test-token")
+			},
+			expectedToken: "Bot test-token",
+			validatePreserve: func(t *testing.T, session *discordgo.Session) {
+				require.NotNil(t, session.Client, "HTTP client should not be nil")
+				require.Equal(t, 45*time.Second, session.Client.Timeout, "Custom timeout should be preserved")
+			},
+		},
+		{
+			name: "AuthenticateWithOAuth2Token preserves custom settings",
+			setupSession: func() *discordgo.Session {
+				session := DefaultSession()
+				session.ShardID = 3
+				session.ShardCount = 10
+				return session
+			},
+			authMethod: func(d *Discord) error {
+				return d.AuthenticateWithOAuth2Token("oauth-token")
+			},
+			expectedToken: "Bearer oauth-token",
+			validatePreserve: func(t *testing.T, session *discordgo.Session) {
+				require.Equal(t, 3, session.ShardID, "ShardID should be preserved")
+				require.Equal(t, 10, session.ShardCount, "ShardCount should be preserved")
+			},
+		},
+		{
+			name: "Multiple custom settings preserved",
+			setupSession: func() *discordgo.Session {
+				session := DefaultSession()
+				session.Client = &http.Client{Timeout: 60 * time.Second}
+				session.ShardID = 2
+				session.MaxRestRetries = 5
+				return session
+			},
+			authMethod: func(d *Discord) error {
+				return d.AuthenticateWithBotToken("multi-token")
+			},
+			expectedToken: "Bot multi-token",
+			validatePreserve: func(t *testing.T, session *discordgo.Session) {
+				require.Equal(t, 60*time.Second, session.Client.Timeout, "Timeout should be preserved")
+				require.Equal(t, 2, session.ShardID, "ShardID should be preserved")
+				require.Equal(t, 5, session.MaxRestRetries, "MaxRestRetries should be preserved")
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			session := tt.setupSession()
+			d := New()
+			d.SetClient(session)
+
+			err := tt.authMethod(d)
+			require.NoError(t, err, "Authentication should succeed")
+
+			// Verify token was set correctly
+			sessionAfter, ok := d.client.(*discordgo.Session)
+			require.True(t, ok, "client should be a *discordgo.Session")
+			require.Equal(t, tt.expectedToken, sessionAfter.Token, "Token should be set correctly")
+			require.Equal(t, tt.expectedToken, sessionAfter.Identify.Token, "Identify.Token should be set correctly")
+
+			// Verify intents were set
+			require.Equal(
+				t,
+				discordgo.IntentsGuildMessageTyping,
+				sessionAfter.Identify.Intents,
+				"Intents should be set",
+			)
+
+			// Verify custom settings were preserved
+			tt.validatePreserve(t, sessionAfter)
+		})
+	}
+}
+
+func TestDiscord_AuthenticateWithoutSetClient(t *testing.T) {
+	t.Parallel()
+
+	// Test that authentication still works without calling SetClient
+	d := New()
+	err := d.AuthenticateWithBotToken("standard-token")
+	require.NoError(t, err, "Authentication should succeed without SetClient")
+
+	session, ok := d.client.(*discordgo.Session)
+	require.True(t, ok, "client should be a *discordgo.Session")
+	require.Equal(t, "Bot standard-token", session.Token, "Token should be set correctly")
+	require.Equal(t, discordgo.IntentsGuildMessageTyping, session.Identify.Intents, "Intents should be set")
+}