Spaces:
Sleeping
Sleeping
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
- pkg/channels/telegram.go +39 -24
- 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 |
-
//
|
| 86 |
-
|
|
|
|
| 87 |
|
| 88 |
-
|
|
|
|
| 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
|
| 109 |
-
if err := channel.
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|