personalbotai commited on
Commit
f5fca4e
·
1 Parent(s): 2cbc70b

feat: implement Telegram proxy support for Hugging Face Spaces

Browse files

- add NewProxiedHTTPClient() with TELEGRAM_PROXY_URL, HTTPS_PROXY, HTTP_PROXY support
- add IsProxyOrNetworkError() helper to detect network/proxy failures
- implement proxy-first with IPv4 fallback strategy
- test connectivity with both proxy and direct connection
- recreate bot with appropriate client based on connectivity test
- log connection method (proxy vs direct) for observability

Fixes: Telegram blocked by Hugging Face network policy - now works with proxy

Closes: #telegram-proxy

Files changed (2) hide show
  1. pkg/channels/telegram.go +39 -24
  2. pkg/utils/http.go +66 -0
pkg/channels/telegram.go CHANGED
@@ -11,10 +11,9 @@ import (
11
  "sync"
12
  "time"
13
 
14
- th "github.com/mymmrac/telego/telegohandler"
15
-
16
  "github.com/mymmrac/telego"
17
  "github.com/mymmrac/telego/telegohandler"
 
18
  tu "github.com/mymmrac/telego/telegoutil"
19
 
20
  "github.com/sipeed/picoclaw/pkg/bus"
@@ -51,12 +50,15 @@ func (c *thinkingCancel) Cancel() {
51
 
52
  // testConnectivity checks if we can reach Telegram API (bypasses network policy blocks)
53
  func (c *TelegramChannel) testConnectivity() error {
 
 
 
 
 
54
  // Quick test: try to get bot info (lightweight endpoint)
55
  ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
56
  defer cancel()
57
 
58
- // Use our own HTTP client (same as bot uses)
59
- client := utils.NewIPv4OnlyHTTPClient()
60
  url := "https://api.telegram.org/bot" + c.bot.Token() + "/getMe"
61
 
62
  req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
@@ -77,15 +79,16 @@ func (c *TelegramChannel) testConnectivity() error {
77
  return nil
78
  }
79
 
80
- // NewTelegramChannel creates a Telegram channel with connectivity check
81
  func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChannel, error) {
82
- var opts []telego.BotOption
83
  telegramCfg := cfg.Channels.Telegram
84
 
85
- // Use IPv4-only HTTP client to avoid IPv6 connectivity issues
86
- opts = append(opts, telego.WithHTTPClient(utils.NewIPv4OnlyHTTPClient()))
 
87
 
88
- bot, err := telego.NewBot(telegramCfg.Token, opts...)
 
89
  if err != nil {
90
  return nil, fmt.Errorf("failed to create telegram bot: %w", err)
91
  }
@@ -105,23 +108,35 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann
105
  webhookCancel: nil,
106
  }
107
 
108
- // Test connectivity before fully initializing
109
- if err := channel.testConnectivity(); err != nil {
110
- // Check if it's a network policy block
111
- if strings.Contains(err.Error(), "operation not permitted") ||
112
- strings.Contains(err.Error(), "permission denied") ||
113
- strings.Contains(err.Error(), "network is unreachable") {
114
- logger.ErrorCF("telegram", "Telegram API blocked by network policy", map[string]interface{}{
115
- "error": err.Error(),
116
- "solution": "Hugging Face Spaces blocks Telegram IP ranges. Telegram channel disabled.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  })
118
- // Return error but with clear message - main will handle gracefully
119
- return nil, fmt.Errorf("telegram connectivity failed: %w (Hugging Face network policy blocks Telegram API)", err)
120
  }
121
- // Other errors - still log but continue (might recover)
122
- logger.WarnCF("telegram", "Connectivity test warning", map[string]interface{}{
123
- "error": err.Error(),
124
- })
125
  }
126
 
127
  return channel, nil
 
11
  "sync"
12
  "time"
13
 
 
 
14
  "github.com/mymmrac/telego"
15
  "github.com/mymmrac/telego/telegohandler"
16
+ th "github.com/mymmrac/telego/telegohandler"
17
  tu "github.com/mymmrac/telego/telegoutil"
18
 
19
  "github.com/sipeed/picoclaw/pkg/bus"
 
50
 
51
  // testConnectivity checks if we can reach Telegram API (bypasses network policy blocks)
52
  func (c *TelegramChannel) testConnectivity() error {
53
+ return c.testConnectivityWithClient(utils.NewProxiedHTTPClient())
54
+ }
55
+
56
+ // testConnectivityWithClient tests connectivity using a specific HTTP client
57
+ func (c *TelegramChannel) testConnectivityWithClient(client *http.Client) error {
58
  // Quick test: try to get bot info (lightweight endpoint)
59
  ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
60
  defer cancel()
61
 
 
 
62
  url := "https://api.telegram.org/bot" + c.bot.Token() + "/getMe"
63
 
64
  req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
 
79
  return nil
80
  }
81
 
82
+ // NewTelegramChannel creates a Telegram channel with connectivity check and proxy support
83
  func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChannel, error) {
 
84
  telegramCfg := cfg.Channels.Telegram
85
 
86
+ // Create both client types: with proxy (if configured) and IPv4-only fallback
87
+ proxiedClient := utils.NewProxiedHTTPClient()
88
+ directClient := utils.NewIPv4OnlyHTTPClient()
89
 
90
+ // Try with proxy first if configured
91
+ bot, err := telego.NewBot(telegramCfg.Token, telego.WithHTTPClient(proxiedClient))
92
  if err != nil {
93
  return nil, fmt.Errorf("failed to create telegram bot: %w", err)
94
  }
 
108
  webhookCancel: nil,
109
  }
110
 
111
+ // Test connectivity with proxy first; if fails, try direct connection
112
+ if err := channel.testConnectivityWithClient(proxiedClient); err != nil {
113
+ if utils.IsProxyOrNetworkError(err) {
114
+ // Proxy failed, try direct connection as fallback
115
+ logger.WarnCF("telegram", "Proxy connection failed, falling back to direct", map[string]interface{}{
116
+ "proxy_error": err.Error(),
117
+ })
118
+ if err := channel.testConnectivityWithClient(directClient); err != nil {
119
+ // Both proxy and direct failed
120
+ return nil, fmt.Errorf("telegram connectivity failed (proxy: %v, direct: %v)", err, err)
121
+ }
122
+ // Direct connection works, recreate bot with direct client
123
+ bot2, err2 := telego.NewBot(telegramCfg.Token, telego.WithHTTPClient(directClient))
124
+ if err2 != nil {
125
+ return nil, fmt.Errorf("failed to recreate bot with direct client: %w", err2)
126
+ }
127
+ channel.bot = bot2
128
+ logger.InfoCF("telegram", "Using direct IPv4 connection (proxy configured but unreachable)", nil)
129
+ } else {
130
+ // Non-network error (invalid token, etc.)
131
+ return nil, fmt.Errorf("telegram connectivity failed: %w", err)
132
+ }
133
+ } else {
134
+ // Proxy connection succeeded
135
+ if proxyURL := os.Getenv("TELEGRAM_PROXY_URL"); proxyURL != "" {
136
+ logger.InfoCF("telegram", "Using proxy connection", map[string]interface{}{
137
+ "proxy": proxyURL,
138
  })
 
 
139
  }
 
 
 
 
140
  }
141
 
142
  return channel, nil
pkg/utils/http.go CHANGED
@@ -4,6 +4,8 @@ import (
4
  "context"
5
  "net"
6
  "net/http"
 
 
7
  "strings"
8
  "time"
9
  )
@@ -88,3 +90,67 @@ func NewIPv4OnlyHTTPClient() *http.Client {
88
  Transport: transport,
89
  }
90
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  "context"
5
  "net"
6
  "net/http"
7
+ "net/url"
8
+ "os"
9
  "strings"
10
  "time"
11
  )
 
90
  Transport: transport,
91
  }
92
  }
93
+
94
+ // NewProxiedHTTPClient creates an HTTP client that uses a proxy if configured
95
+ // It respects standard proxy environment variables and adds proxy support for HTTPS
96
+ func NewProxiedHTTPClient() *http.Client {
97
+ // Check for proxy configuration
98
+ proxyURL := os.Getenv("TELEGRAM_PROXY_URL")
99
+ if proxyURL == "" {
100
+ proxyURL = os.Getenv("HTTPS_PROXY")
101
+ if proxyURL == "" {
102
+ proxyURL = os.Getenv("HTTP_PROXY")
103
+ }
104
+ }
105
+
106
+ transport := &http.Transport{
107
+ ForceAttemptHTTP2: true,
108
+ MaxIdleConns: 100,
109
+ IdleConnTimeout: 90 * time.Second,
110
+ TLSHandshakeTimeout: 10 * time.Second,
111
+ ExpectContinueTimeout: 1 * time.Second,
112
+ }
113
+
114
+ // If proxy is configured, set it up
115
+ if proxyURL != "" {
116
+ u, err := url.Parse(proxyURL)
117
+ if err != nil {
118
+ // If proxy URL is invalid, log and continue without proxy
119
+ // In production, we'd use a logger but this is a low-level utils function
120
+ // So we'll just proceed without proxy
121
+ } else {
122
+ transport.Proxy = http.ProxyURL(u)
123
+ }
124
+ }
125
+
126
+ return &http.Client{
127
+ Transport: transport,
128
+ }
129
+ }
130
+
131
+ // IsProxyOrNetworkError checks if an error indicates proxy or network policy block
132
+ func IsProxyOrNetworkError(err error) bool {
133
+ if err == nil {
134
+ return false
135
+ }
136
+ errStr := err.Error()
137
+ // Common network/block indicators
138
+ indicators := []string{
139
+ "operation not permitted",
140
+ "permission denied",
141
+ "network is unreachable",
142
+ "connection refused",
143
+ "timeout",
144
+ "no route to host",
145
+ "i/o timeout",
146
+ "dial tcp",
147
+ "connect:",
148
+ "proxy",
149
+ }
150
+ for _, indicator := range indicators {
151
+ if strings.Contains(strings.ToLower(errStr), indicator) {
152
+ return true
153
+ }
154
+ }
155
+ return false
156
+ }