3 Commitit e5c0fe3edf ... 5b796672e9

Tekijä SHA1 Viesti Päivämäärä
  MHSanaei 5b796672e9 Improve telego client robustness and retries 4 päivää sitten
  MHSanaei 3fa0da38c9 Add timeouts and delays to backup sends 4 päivää sitten
  MHSanaei 8eb1225734 translate bug fix #3789 4 päivää sitten

+ 2 - 2
sub/subController.go

@@ -143,11 +143,11 @@ func (a *SUBController) subs(c *gin.Context) {
 
 		// Add headers
 		header := fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
-		profileUrl := a.subProfileUrl  
+		profileUrl := a.subProfileUrl
 		if profileUrl == "" {
 			profileUrl = fmt.Sprintf("%s://%s%s", scheme, hostWithPort, c.Request.RequestURI)
 		}
-		a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle, a.subSupportUrl, profileUrl, a.subAnnounce, a.subEnableRouting, a.subRoutingRules)  
+		a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle, a.subSupportUrl, profileUrl, a.subAnnounce, a.subEnableRouting, a.subRoutingRules)
 
 		if a.subEncrypt {
 			c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))

+ 6 - 6
web/service/inbound.go

@@ -2141,25 +2141,25 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error)
 	if err != nil {
 		return "", err
 	}
-	
+
 	if InboundClientIps.Ips == "" {
 		return "", nil
 	}
-	
+
 	// Try to parse as new format (with timestamps)
 	type IPWithTimestamp struct {
 		IP        string `json:"ip"`
 		Timestamp int64  `json:"timestamp"`
 	}
-	
+
 	var ipsWithTime []IPWithTimestamp
 	err = json.Unmarshal([]byte(InboundClientIps.Ips), &ipsWithTime)
-	
+
 	// If successfully parsed as new format, return with timestamps
 	if err == nil && len(ipsWithTime) > 0 {
 		return InboundClientIps.Ips, nil
 	}
-	
+
 	// Otherwise, assume it's old format (simple string array)
 	// Try to parse as simple array and convert to new format
 	var oldIps []string
@@ -2176,7 +2176,7 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error)
 		result, _ := json.Marshal(newIpsWithTime)
 		return string(result), nil
 	}
-	
+
 	// Return as-is if parsing fails
 	return InboundClientIps.Ips, nil
 }

+ 20 - 20
web/service/setting.go

@@ -5,7 +5,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-    "net"
+	"net"
 	"reflect"
 	"strconv"
 	"strings"
@@ -719,25 +719,25 @@ func (s *SettingService) GetDefaultXrayConfig() (any, error) {
 }
 
 func extractHostname(host string) string {
-    h, _, err := net.SplitHostPort(host)
-    // Err is not nil means host does not contain port
-    if err != nil {
-        h = host
-    }
-
-    ip := net.ParseIP(h)
-    // If it's not an IP, return as is
-    if ip == nil {
-        return h
-    }
-
-    // If it's an IPv4, return as is
-    if ip.To4() != nil {
-        return h
-    }
-
-    // IPv6 needs bracketing
-    return "[" + h + "]"
+	h, _, err := net.SplitHostPort(host)
+	// Err is not nil means host does not contain port
+	if err != nil {
+		h = host
+	}
+
+	ip := net.ParseIP(h)
+	// If it's not an IP, return as is
+	if ip == nil {
+		return h
+	}
+
+	// If it's an IPv4, return as is
+	if ip.To4() != nil {
+		return h
+	}
+
+	// IPv6 needs bracketing
+	return "[" + h + "]"
 }
 
 func (s *SettingService) GetDefaultSettings(host string) (any, error) {

+ 109 - 31
web/service/tgbot.go

@@ -272,41 +272,78 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
 	return nil
 }
 
-// NewBot creates a new Telegram bot instance with optional proxy and API server settings.
-func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
-	if proxyUrl == "" && apiServerUrl == "" {
-		return telego.NewBot(token)
+// createRobustFastHTTPClient creates a fasthttp.Client with proper connection handling
+func (t *Tgbot) createRobustFastHTTPClient(proxyUrl string) *fasthttp.Client {
+	client := &fasthttp.Client{
+		// Connection timeouts
+		ReadTimeout:                   30 * time.Second,
+		WriteTimeout:                  30 * time.Second,
+		MaxIdleConnDuration:           60 * time.Second,
+		MaxConnDuration:               0, // unlimited, but controlled by MaxIdleConnDuration
+		MaxIdemponentCallAttempts:     3,
+		ReadBufferSize:                4096,
+		WriteBufferSize:               4096,
+		MaxConnsPerHost:               100,
+		MaxConnWaitTimeout:            10 * time.Second,
+		DisableHeaderNamesNormalizing: false,
+		DisablePathNormalizing:        false,
+		// Retry on connection errors
+		RetryIf: func(request *fasthttp.Request) bool {
+			// Retry on connection errors for GET requests
+			return string(request.Header.Method()) == "GET" || string(request.Header.Method()) == "POST"
+		},
+	}
+
+	// Set proxy if provided
+	if proxyUrl != "" {
+		client.Dial = fasthttpproxy.FasthttpSocksDialer(proxyUrl)
 	}
 
+	return client
+}
+
+// NewBot creates a new Telegram bot instance with optional proxy and API server settings.
+func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
+	// Validate proxy URL if provided
 	if proxyUrl != "" {
 		if !strings.HasPrefix(proxyUrl, "socks5://") {
-			logger.Warning("Invalid socks5 URL, using default")
-			return telego.NewBot(token)
+			logger.Warning("Invalid socks5 URL, ignoring proxy")
+			proxyUrl = "" // Clear invalid proxy
+		} else {
+			_, err := url.Parse(proxyUrl)
+			if err != nil {
+				logger.Warningf("Can't parse proxy URL, ignoring proxy: %v", err)
+				proxyUrl = ""
+			}
 		}
+	}
 
-		_, err := url.Parse(proxyUrl)
-		if err != nil {
-			logger.Warningf("Can't parse proxy URL, using default instance for tgbot: %v", err)
-			return telego.NewBot(token)
+	// Validate API server URL if provided
+	if apiServerUrl != "" {
+		if !strings.HasPrefix(apiServerUrl, "http") {
+			logger.Warning("Invalid http(s) URL for API server, using default")
+			apiServerUrl = ""
+		} else {
+			_, err := url.Parse(apiServerUrl)
+			if err != nil {
+				logger.Warningf("Can't parse API server URL, using default: %v", err)
+				apiServerUrl = ""
+			}
 		}
-
-		return telego.NewBot(token, telego.WithFastHTTPClient(&fasthttp.Client{
-			Dial: fasthttpproxy.FasthttpSocksDialer(proxyUrl),
-		}))
 	}
 
-	if !strings.HasPrefix(apiServerUrl, "http") {
-		logger.Warning("Invalid http(s) URL, using default")
-		return telego.NewBot(token)
-	}
+	// Create robust fasthttp client
+	client := t.createRobustFastHTTPClient(proxyUrl)
 
-	_, err := url.Parse(apiServerUrl)
-	if err != nil {
-		logger.Warningf("Can't parse API server URL, using default instance for tgbot: %v", err)
-		return telego.NewBot(token)
+	// Build bot options
+	var options []telego.BotOption
+	options = append(options, telego.WithFastHTTPClient(client))
+
+	if apiServerUrl != "" {
+		options = append(options, telego.WithAPIServer(apiServerUrl))
 	}
 
-	return telego.NewBot(token, telego.WithAPIServer(apiServerUrl))
+	return telego.NewBot(token, options...)
 }
 
 // IsRunning checks if the Telegram bot is currently running.
@@ -390,7 +427,7 @@ func (t *Tgbot) decodeQuery(query string) (string, error) {
 // OnReceive starts the message receiving loop for the Telegram bot.
 func (t *Tgbot) OnReceive() {
 	params := telego.GetUpdatesParams{
-		Timeout: 30, // Increased timeout to reduce API calls
+		Timeout: 20, // Reduced timeout to detect connection issues faster
 	}
 	// Strict singleton: never start a second long-polling loop.
 	tgBotMutex.Lock()
@@ -408,7 +445,7 @@ func (t *Tgbot) OnReceive() {
 	botWG.Add(1)
 	tgBotMutex.Unlock()
 
-	// Get updates channel using the context.
+	// Get updates channel using the context with shorter timeout for better error recovery
 	updates, _ := bot.UpdatesViaLongPolling(ctx, &params)
 	go func() {
 		defer botWG.Done()
@@ -2247,10 +2284,36 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
 		if len(replyMarkup) > 0 && n == (len(allMessages)-1) {
 			params.ReplyMarkup = replyMarkup[0]
 		}
-		_, err := bot.SendMessage(context.Background(), &params)
-		if err != nil {
-			logger.Warning("Error sending telegram message :", err)
+
+		// Retry logic with exponential backoff for connection errors
+		maxRetries := 3
+		for attempt := range maxRetries {
+			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+			_, err := bot.SendMessage(ctx, &params)
+			cancel()
+
+			if err == nil {
+				break // Success
+			}
+
+			// Check if error is a connection error
+			errStr := err.Error()
+			isConnectionError := strings.Contains(errStr, "connection") ||
+				strings.Contains(errStr, "timeout") ||
+				strings.Contains(errStr, "closed")
+
+			if isConnectionError && attempt < maxRetries-1 {
+				// Exponential backoff: 1s, 2s, 4s
+				backoff := time.Duration(1<<uint(attempt)) * time.Second
+				logger.Warningf("Connection error sending telegram message (attempt %d/%d), retrying in %v: %v",
+					attempt+1, maxRetries, backoff, err)
+				time.Sleep(backoff)
+			} else {
+				logger.Warning("Error sending telegram message:", err)
+				break
+			}
 		}
+
 		// Reduced delay to improve performance (only needed for rate limiting)
 		if n < len(allMessages)-1 { // Only delay between messages, not after the last one
 			time.Sleep(100 * time.Millisecond)
@@ -2585,8 +2648,12 @@ func (t *Tgbot) SendBackupToAdmins() {
 	if !t.IsRunning() {
 		return
 	}
-	for _, adminId := range adminIds {
+	for i, adminId := range adminIds {
 		t.sendBackup(int64(adminId))
+		// Add delay between sends to avoid Telegram rate limits
+		if i < len(adminIds)-1 {
+			time.Sleep(1 * time.Second)
+		}
 	}
 }
 
@@ -3596,13 +3663,17 @@ func (t *Tgbot) sendBackup(chatId int64) {
 		logger.Error("Error in trigger a checkpoint operation: ", err)
 	}
 
+	// Send database backup
 	file, err := os.Open(config.GetDBPath())
 	if err == nil {
+		defer file.Close()
+		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+		defer cancel()
 		document := tu.Document(
 			tu.ID(chatId),
 			tu.File(file),
 		)
-		_, err = bot.SendDocument(context.Background(), document)
+		_, err = bot.SendDocument(ctx, document)
 		if err != nil {
 			logger.Error("Error in uploading backup: ", err)
 		}
@@ -3610,13 +3681,20 @@ func (t *Tgbot) sendBackup(chatId int64) {
 		logger.Error("Error in opening db file for backup: ", err)
 	}
 
+	// Small delay between file sends
+	time.Sleep(500 * time.Millisecond)
+
+	// Send config.json backup
 	file, err = os.Open(xray.GetConfigPath())
 	if err == nil {
+		defer file.Close()
+		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+		defer cancel()
 		document := tu.Document(
 			tu.ID(chatId),
 			tu.File(file),
 		)
-		_, err = bot.SendDocument(context.Background(), document)
+		_, err = bot.SendDocument(ctx, document)
 		if err != nil {
 			logger.Error("Error in uploading config.json: ", err)
 		}

+ 6 - 0
web/translation/translate.ar_EG.toml

@@ -525,6 +525,12 @@
 "accountInfo" = "معلومات الحساب"
 "outboundStatus" = "حالة المخرج"
 "sendThrough" = "أرسل من خلال"
+"test" = "اختبار"
+"testResult" = "نتيجة الاختبار"
+"testing" = "جاري اختبار الاتصال..."
+"testSuccess" = "الاختبار ناجح"
+"testFailed" = "فشل الاختبار"
+"testError" = "فشل اختبار المخرج"
 
 [pages.xray.balancer]
 "addBalancer" = "أضف موازن تحميل"

+ 6 - 0
web/translation/translate.es_ES.toml

@@ -525,6 +525,12 @@
 "accountInfo" = "Información de la Cuenta"
 "outboundStatus" = "Estado de Salida"
 "sendThrough" = "Enviar a través de"
+"test" = "Probar"
+"testResult" = "Resultado de la prueba"
+"testing" = "Probando conexión..."
+"testSuccess" = "Prueba exitosa"
+"testFailed" = "Prueba fallida"
+"testError" = "Error al probar la salida"
 
 [pages.xray.balancer]
 "addBalancer" = "Agregar equilibrador"

+ 6 - 0
web/translation/translate.fa_IR.toml

@@ -525,6 +525,12 @@
 "accountInfo" = "اطلاعات حساب"
 "outboundStatus" = "وضعیت خروجی"
 "sendThrough" = "ارسال با"
+"test" = "تست"
+"testResult" = "نتیجه تست"
+"testing" = "در حال تست اتصال..."
+"testSuccess" = "تست موفقیت‌آمیز"
+"testFailed" = "تست ناموفق"
+"testError" = "خطا در تست خروجی"
 
 [pages.xray.balancer]
 "addBalancer" = "افزودن بالانسر"

+ 6 - 0
web/translation/translate.id_ID.toml

@@ -525,6 +525,12 @@
 "accountInfo" = "Informasi Akun"
 "outboundStatus" = "Status Keluar"
 "sendThrough" = "Kirim Melalui"
+"test" = "Tes"
+"testResult" = "Hasil Tes"
+"testing" = "Menguji koneksi..."
+"testSuccess" = "Tes berhasil"
+"testFailed" = "Tes gagal"
+"testError" = "Gagal menguji outbound"
 
 [pages.xray.balancer]
 "addBalancer" = "Tambahkan Penyeimbang"

+ 6 - 0
web/translation/translate.ja_JP.toml

@@ -525,6 +525,12 @@
 "accountInfo" = "アカウント情報"
 "outboundStatus" = "アウトバウンドステータス"
 "sendThrough" = "送信経路"
+"test" = "テスト"
+"testResult" = "テスト結果"
+"testing" = "接続をテスト中..."
+"testSuccess" = "テスト成功"
+"testFailed" = "テスト失敗"
+"testError" = "アウトバウンドのテストに失敗しました"
 
 [pages.xray.balancer]
 "addBalancer" = "負荷分散追加"

+ 6 - 0
web/translation/translate.pt_BR.toml

@@ -525,6 +525,12 @@
 "accountInfo" = "Informações da Conta"
 "outboundStatus" = "Status de Saída"
 "sendThrough" = "Enviar Através de"
+"test" = "Testar"
+"testResult" = "Resultado do teste"
+"testing" = "Testando conexão..."
+"testSuccess" = "Teste bem-sucedido"
+"testFailed" = "Teste falhou"
+"testError" = "Falha ao testar saída"
 
 [pages.xray.balancer]
 "addBalancer" = "Adicionar Balanceador"

+ 6 - 0
web/translation/translate.ru_RU.toml

@@ -525,6 +525,12 @@
 "accountInfo" = "Информация об учетной записи"
 "outboundStatus" = "Статус исходящего подключения"
 "sendThrough" = "Отправить через"
+"test" = "Тест"
+"testResult" = "Результат теста"
+"testing" = "Тестирование соединения..."
+"testSuccess" = "Тест успешен"
+"testFailed" = "Тест не пройден"
+"testError" = "Не удалось протестировать исходящее подключение"
 
 [pages.xray.balancer]
 "addBalancer" = "Создать балансировщик"

+ 6 - 0
web/translation/translate.tr_TR.toml

@@ -525,6 +525,12 @@
 "accountInfo" = "Hesap Bilgileri"
 "outboundStatus" = "Giden Durumu"
 "sendThrough" = "Üzerinden Gönder"
+"test" = "Test"
+"testResult" = "Test Sonucu"
+"testing" = "Bağlantı test ediliyor..."
+"testSuccess" = "Test başarılı"
+"testFailed" = "Test başarısız"
+"testError" = "Giden test edilemedi"
 
 [pages.xray.balancer]
 "addBalancer" = "Dengeleyici Ekle"

+ 6 - 0
web/translation/translate.uk_UA.toml

@@ -525,6 +525,12 @@
 "accountInfo" = "Інформація про обліковий запис"
 "outboundStatus" = "Статус виходу"
 "sendThrough" = "Надіслати через"
+"test" = "Тест"
+"testResult" = "Результат тесту"
+"testing" = "Тестування з'єднання..."
+"testSuccess" = "Тест успішний"
+"testFailed" = "Тест не пройдено"
+"testError" = "Не вдалося протестувати вихідне з'єднання"
 
 [pages.xray.balancer]
 "addBalancer" = "Додати балансир"

+ 6 - 0
web/translation/translate.vi_VN.toml

@@ -525,6 +525,12 @@
 "accountInfo" = "Thông tin tài khoản"
 "outboundStatus" = "Trạng thái đầu ra"
 "sendThrough" = "Gửi qua"
+"test" = "Kiểm tra"
+"testResult" = "Kết quả kiểm tra"
+"testing" = "Đang kiểm tra kết nối..."
+"testSuccess" = "Kiểm tra thành công"
+"testFailed" = "Kiểm tra thất bại"
+"testError" = "Không thể kiểm tra đầu ra"
 
 [pages.xray.balancer]
 "addBalancer" = "Thêm cân bằng"

+ 6 - 0
web/translation/translate.zh_CN.toml

@@ -525,6 +525,12 @@
 "accountInfo" = "帐户信息"
 "outboundStatus" = "出站状态"
 "sendThrough" = "发送通过"
+"test" = "测试"
+"testResult" = "测试结果"
+"testing" = "正在测试连接..."
+"testSuccess" = "测试成功"
+"testFailed" = "测试失败"
+"testError" = "测试出站失败"
 
 [pages.xray.balancer]
 "addBalancer" = "添加负载均衡"

+ 6 - 0
web/translation/translate.zh_TW.toml

@@ -525,6 +525,12 @@
 "accountInfo" = "帳戶資訊"
 "outboundStatus" = "出站狀態"
 "sendThrough" = "傳送通過"
+"test" = "測試"
+"testResult" = "測試結果"
+"testing" = "正在測試連接..."
+"testSuccess" = "測試成功"
+"testFailed" = "測試失敗"
+"testError" = "測試出站失敗"
 
 [pages.xray.balancer]
 "addBalancer" = "新增負載均衡"