Browse Source

Merge branch 'main' into logs_fixes

Sanaei 3 days ago
parent
commit
e1d5fce55b

+ 1 - 1
Dockerfile

@@ -1,7 +1,7 @@
 # ========================================================
 # Stage: Builder
 # ========================================================
-FROM golang:1.24-alpine AS builder
+FROM golang:1.25-alpine AS builder
 WORKDIR /app
 ARG TARGETARCH
 

+ 4 - 4
go.mod

@@ -1,6 +1,6 @@
 module x-ui
 
-go 1.24.5
+go 1.25.0
 
 require (
 	github.com/gin-contrib/gzip v1.2.3
@@ -15,7 +15,7 @@ require (
 	github.com/pelletier/go-toml/v2 v2.2.4
 	github.com/robfig/cron/v3 v3.0.1
 	github.com/shirou/gopsutil/v4 v4.25.7
-	github.com/valyala/fasthttp v1.64.0
+	github.com/valyala/fasthttp v1.65.0
 	github.com/xlzd/gotp v0.1.0
 	github.com/xtls/xray-core v1.250803.0
 	go.uber.org/atomic v1.11.0
@@ -56,7 +56,7 @@ require (
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
-	github.com/mattn/go-sqlite3 v1.14.30 // indirect
+	github.com/mattn/go-sqlite3 v1.14.32 // indirect
 	github.com/miekg/dns v1.1.68 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -92,7 +92,7 @@ require (
 	golang.org/x/tools v0.36.0 // indirect
 	golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
 	golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
 	google.golang.org/protobuf v1.36.7 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect

+ 6 - 6
go.sum

@@ -95,8 +95,8 @@ github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr32
 github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY=
-github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
+github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
 github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -162,8 +162,8 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF
 github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasthttp v1.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og=
-github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA=
+github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
+github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4=
 github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
 github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
 github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
@@ -226,8 +226,8 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
 golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
 golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
 google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
 google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
 google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=

+ 3 - 2
sub/subJsonService.go

@@ -209,9 +209,10 @@ func (s *SubJsonService) streamData(stream string) map[string]any {
 	var streamSettings map[string]any
 	json.Unmarshal([]byte(stream), &streamSettings)
 	security, _ := streamSettings["security"].(string)
-	if security == "tls" {
+	switch security {
+	case "tls":
 		streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]any))
-	} else if security == "reality" {
+	case "reality":
 		streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]any))
 	}
 	delete(streamSettings, "sockopt")

+ 1 - 1
web/assets/js/model/outbound.js

@@ -995,7 +995,7 @@ Outbound.DNSSettings = class extends CommonClass {
         network = 'udp',
         address = '',
         port = 53,
-        nonIPQuery = 'drop',
+        nonIPQuery = 'reject',
         blockTypes = []
     ) {
         super();

+ 3 - 3
web/entity/entity.go

@@ -2,10 +2,10 @@ package entity
 
 import (
 	"crypto/tls"
+	"math"
 	"net"
 	"strings"
 	"time"
-	"math"
 
 	"x-ui/util/common"
 )
@@ -39,8 +39,8 @@ type AllSetting struct {
 	TgCpu                       int    `json:"tgCpu" form:"tgCpu"`
 	TgLang                      string `json:"tgLang" form:"tgLang"`
 	TimeLocation                string `json:"timeLocation" form:"timeLocation"`
-	TwoFactorEnable				bool   `json:"twoFactorEnable" form:"twoFactorEnable"`
-	TwoFactorToken				string `json:"twoFactorToken" form:"twoFactorToken"`
+	TwoFactorEnable             bool   `json:"twoFactorEnable" form:"twoFactorEnable"`
+	TwoFactorToken              string `json:"twoFactorToken" form:"twoFactorToken"`
 	SubEnable                   bool   `json:"subEnable" form:"subEnable"`
 	SubTitle                    string `json:"subTitle" form:"subTitle"`
 	SubListen                   string `json:"subListen" form:"subListen"`

+ 1 - 1
web/html/form/outbound.html

@@ -105,7 +105,7 @@
         </a-form-item>
         <a-form-item label='non-IP queries'>
           <a-select v-model="outbound.settings.nonIPQuery" :dropdown-class-name="themeSwitcher.currentTheme">
-            <a-select-option v-for="s in ['drop','skip']" :value="s">[[ s ]]</a-select-option>
+            <a-select-option v-for="s in ['reject','drop','skip']" :value="s">[[ s ]]</a-select-option>
           </a-select>
         </a-form-item>
         <a-form-item v-if="outbound.settings.nonIPQuery === 'skip'" label='Block Types' >

+ 3 - 8
web/job/check_client_ip_job.go

@@ -11,7 +11,6 @@ import (
 	"sort"
 	"time"
 
-	"slices"
 	"x-ui/database"
 	"x-ui/database/model"
 	"x-ui/logger"
@@ -58,21 +57,21 @@ func (j *CheckClientIpJob) Run() {
 func (j *CheckClientIpJob) clearAccessLog() {
 	logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
 	j.checkError(err)
+	defer logAccessP.Close()
 
 	accessLogPath, err := xray.GetAccessLogPath()
 	j.checkError(err)
 
 	file, err := os.Open(accessLogPath)
 	j.checkError(err)
+	defer file.Close()
 
 	_, err = io.Copy(logAccessP, file)
 	j.checkError(err)
 
-	logAccessP.Close()
-	file.Close()
-
 	err = os.Truncate(accessLogPath, 0)
 	j.checkError(err)
+
 	j.lastClear = time.Now().Unix()
 }
 
@@ -193,10 +192,6 @@ func (j *CheckClientIpJob) checkError(e error) {
 	}
 }
 
-func (j *CheckClientIpJob) contains(s []string, str string) bool {
-	return slices.Contains(s, str)
-}
-
 func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
 	db := database.GetDB()
 	InboundClientIps := &model.InboundClientIps{}

+ 32 - 24
web/service/inbound.go

@@ -177,15 +177,16 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
 
 	// Secure client ID
 	for _, client := range clients {
-		if inbound.Protocol == "trojan" {
+		switch inbound.Protocol {
+		case "trojan":
 			if client.Password == "" {
 				return inbound, false, common.NewError("empty client ID")
 			}
-		} else if inbound.Protocol == "shadowsocks" {
+		case "shadowsocks":
 			if client.Email == "" {
 				return inbound, false, common.NewError("empty client ID")
 			}
-		} else {
+		default:
 			if client.ID == "" {
 				return inbound, false, common.NewError("empty client ID")
 			}
@@ -436,15 +437,16 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
 
 	// Secure client ID
 	for _, client := range clients {
-		if oldInbound.Protocol == "trojan" {
+		switch oldInbound.Protocol {
+		case "trojan":
 			if client.Password == "" {
 				return false, common.NewError("empty client ID")
 			}
-		} else if oldInbound.Protocol == "shadowsocks" {
+		case "shadowsocks":
 			if client.Email == "" {
 				return false, common.NewError("empty client ID")
 			}
-		} else {
+		default:
 			if client.ID == "" {
 				return false, common.NewError("empty client ID")
 			}
@@ -631,13 +633,14 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
 	clientIndex := -1
 	for index, oldClient := range oldClients {
 		oldClientId := ""
-		if oldInbound.Protocol == "trojan" {
+		switch oldInbound.Protocol {
+		case "trojan":
 			oldClientId = oldClient.Password
 			newClientId = clients[0].Password
-		} else if oldInbound.Protocol == "shadowsocks" {
+		case "shadowsocks":
 			oldClientId = oldClient.Email
 			newClientId = clients[0].Email
-		} else {
+		default:
 			oldClientId = oldClient.ID
 			newClientId = clients[0].ID
 		}
@@ -1244,11 +1247,12 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (boo
 
 	for _, oldClient := range oldClients {
 		if oldClient.Email == clientEmail {
-			if inbound.Protocol == "trojan" {
+			switch inbound.Protocol {
+			case "trojan":
 				clientId = oldClient.Password
-			} else if inbound.Protocol == "shadowsocks" {
+			case "shadowsocks":
 				clientId = oldClient.Email
-			} else {
+			default:
 				clientId = oldClient.ID
 			}
 			break
@@ -1328,11 +1332,12 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo
 
 	for _, oldClient := range oldClients {
 		if oldClient.Email == clientEmail {
-			if inbound.Protocol == "trojan" {
+			switch inbound.Protocol {
+			case "trojan":
 				clientId = oldClient.Password
-			} else if inbound.Protocol == "shadowsocks" {
+			case "shadowsocks":
 				clientId = oldClient.Email
-			} else {
+			default:
 				clientId = oldClient.ID
 			}
 			clientOldEnabled = oldClient.Enable
@@ -1391,11 +1396,12 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
 
 	for _, oldClient := range oldClients {
 		if oldClient.Email == clientEmail {
-			if inbound.Protocol == "trojan" {
+			switch inbound.Protocol {
+			case "trojan":
 				clientId = oldClient.Password
-			} else if inbound.Protocol == "shadowsocks" {
+			case "shadowsocks":
 				clientId = oldClient.Email
-			} else {
+			default:
 				clientId = oldClient.ID
 			}
 			break
@@ -1448,11 +1454,12 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
 
 	for _, oldClient := range oldClients {
 		if oldClient.Email == clientEmail {
-			if inbound.Protocol == "trojan" {
+			switch inbound.Protocol {
+			case "trojan":
 				clientId = oldClient.Password
-			} else if inbound.Protocol == "shadowsocks" {
+			case "shadowsocks":
 				clientId = oldClient.Email
-			} else {
+			default:
 				clientId = oldClient.ID
 			}
 			break
@@ -1508,11 +1515,12 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
 
 	for _, oldClient := range oldClients {
 		if oldClient.Email == clientEmail {
-			if inbound.Protocol == "trojan" {
+			switch inbound.Protocol {
+			case "trojan":
 				clientId = oldClient.Password
-			} else if inbound.Protocol == "shadowsocks" {
+			case "shadowsocks":
 				clientId = oldClient.Email
-			} else {
+			default:
 				clientId = oldClient.ID
 			}
 			break

+ 75 - 21
web/service/tgbot.go

@@ -40,7 +40,6 @@ var (
 	isRunning   bool
 	hostname    string
 	hashStorage *global.HashStorage
-	handler     *th.Handler
 
 	// clients data to adding new client
 	receiver_inbound_ID int
@@ -641,13 +640,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 						if len(dataArray) == 4 {
 							num, err := strconv.Atoi(dataArray[3])
 							if err == nil {
-								if num == -2 {
+								switch num {
+								case -2:
 									inputNumber = 0
-								} else if num == -1 {
+								case -1:
 									if inputNumber > 0 {
 										inputNumber = (inputNumber / 10)
 									}
-								} else {
+								default:
 									inputNumber = (inputNumber * 10) + num
 								}
 							}
@@ -704,6 +704,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 					return
 				}
 				message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
+				if err != nil {
+					t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
+					return
+				}
 
 				t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
 				t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
@@ -715,13 +719,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 						if len(dataArray) == 3 {
 							num, err := strconv.Atoi(dataArray[2])
 							if err == nil {
-								if num == -2 {
+								switch num {
+								case -2:
 									inputNumber = 0
-								} else if num == -1 {
+								case -1:
 									if inputNumber > 0 {
 										inputNumber = (inputNumber / 10)
 									}
-								} else {
+								default:
 									inputNumber = (inputNumber * 10) + num
 								}
 							}
@@ -844,13 +849,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 						if len(dataArray) == 4 {
 							num, err := strconv.Atoi(dataArray[3])
 							if err == nil {
-								if num == -2 {
+								switch num {
+								case -2:
 									inputNumber = 0
-								} else if num == -1 {
+								case -1:
 									if inputNumber > 0 {
 										inputNumber = (inputNumber / 10)
 									}
-								} else {
+								default:
 									inputNumber = (inputNumber * 10) + num
 								}
 							}
@@ -919,6 +925,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 					return
 				}
 				message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
+				if err != nil {
+					t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
+					return
+				}
 
 				t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
 				t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
@@ -930,13 +940,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 						if len(dataArray) == 3 {
 							num, err := strconv.Atoi(dataArray[2])
 							if err == nil {
-								if num == -2 {
+								switch num {
+								case -2:
 									inputNumber = 0
-								} else if num == -1 {
+								case -1:
 									if inputNumber > 0 {
 										inputNumber = (inputNumber / 10)
 									}
-								} else {
+								default:
 									inputNumber = (inputNumber * 10) + num
 								}
 							}
@@ -1035,13 +1046,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 						if len(dataArray) == 4 {
 							num, err := strconv.Atoi(dataArray[3])
 							if err == nil {
-								if num == -2 {
+								switch num {
+								case -2:
 									inputNumber = 0
-								} else if num == -1 {
+								case -1:
 									if inputNumber > 0 {
 										inputNumber = (inputNumber / 10)
 									}
-								} else {
+								default:
 									inputNumber = (inputNumber * 10) + num
 								}
 							}
@@ -1101,6 +1113,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 					return
 				}
 				message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
+				if err != nil {
+					t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
+					return
+				}
 
 				t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
 				t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
@@ -1112,13 +1128,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 						if len(dataArray) == 3 {
 							num, err := strconv.Atoi(dataArray[2])
 							if err == nil {
-								if num == -2 {
+								switch num {
+								case -2:
 									inputNumber = 0
-								} else if num == -1 {
+								case -1:
 									if inputNumber > 0 {
 										inputNumber = (inputNumber / 10)
 									}
-								} else {
+								default:
 									inputNumber = (inputNumber * 10) + num
 								}
 							}
@@ -1288,6 +1305,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 				}
 
 				message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
+				if err != nil {
+					t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
+					return
+				}
 
 				t.addClient(callbackQuery.Message.GetChat().ID, message_text)
 			}
@@ -1524,6 +1545,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 			return
 		}
 		message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
+		if err != nil {
+			t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
+			return
+		}
 		t.addClient(chatId, message_text, messageId)
 		t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email))
 	case "add_client_default_ip_limit":
@@ -1534,6 +1559,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 			return
 		}
 		message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
+		if err != nil {
+			t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
+			return
+		}
 		t.addClient(chatId, message_text, messageId)
 		t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email))
 	case "add_client_submit_disable":
@@ -1598,6 +1627,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 			return
 		}
 		valid_emails, extra_emails, err := t.inboundService.FilterAndSortClientEmails(emails)
+		if err != nil {
+			t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation"), tu.ReplyKeyboardRemove())
+			return
+		}
 
 		for _, valid_emails := range valid_emails {
 			traffic, err := t.inboundService.GetClientTrafficByEmail(valid_emails)
@@ -1760,6 +1793,10 @@ func (t *Tgbot) SubmitAddClient() (bool, error) {
 	}
 
 	jsonString, err := t.BuildJSONForProtocol(inbound.Protocol)
+	if err != nil {
+		logger.Warning("BuildJSONForProtocol run failed:", err)
+		return false, errors.New("failed to build JSON for protocol")
+	}
 
 	newInbound := &model.Inbound{
 		Id:       receiver_inbound_ID,
@@ -2008,10 +2045,11 @@ func (t *Tgbot) UserLoginNotify(username string, password string, ip string, tim
 	}
 
 	msg := ""
-	if status == LoginSuccess {
+	switch status {
+	case LoginSuccess:
 		msg += t.I18nBot("tgbot.messages.loginSuccess")
 		msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
-	} else if status == LoginFail {
+	case LoginFail:
 		msg += t.I18nBot("tgbot.messages.loginFailed")
 		msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
 		msg += t.I18nBot("tgbot.messages.password", "Password=="+password)
@@ -2171,6 +2209,22 @@ func (t *Tgbot) clientInfoMsg(
 		expiryTime = t.I18nBot("tgbot.unlimited")
 	} else if diff > 172800 || !traffic.Enable {
 		expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
+		if diff > 0 {
+			days := diff / 86400
+			hours := (diff % 86400) / 3600
+			minutes := (diff % 3600) / 60
+			remainingTime := ""
+			if days > 0 {
+				remainingTime += fmt.Sprintf("%d %s ", days, t.I18nBot("tgbot.days"))
+			}
+			if hours > 0 {
+				remainingTime += fmt.Sprintf("%d %s ", hours, t.I18nBot("tgbot.hours"))
+			}
+			if minutes > 0 {
+				remainingTime += fmt.Sprintf("%d %s", minutes, t.I18nBot("tgbot.minutes"))
+			}
+			expiryTime += fmt.Sprintf(" (%s)", remainingTime)
+		}
 	} else if traffic.ExpiryTime < 0 {
 		expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
 		flag = true

+ 14 - 13
web/translation/translate.ar_EG.toml

@@ -561,24 +561,25 @@
 "resetOutboundTrafficError" = "خطأ في إعادة تعيين حركات المرور الصادرة"
 
 [tgbot]
-"keyboardClosed" = "❌ الكيبورد المخصص اتقفلت!"
-"noResult" = "❗ مفيش نتيجة!"
-"noQuery" = "❌ مش لاقي السؤال! استخدم الأمر تاني!"
-"wentWrong" = "❌ حصل خطأ!"
-"noIpRecord" = "❗ مفيش سجل IP!"
-"noInbounds" = "❗ مفيش إدخال متواجد!"
-"unlimited" = "♾ غير محدود (إعادة ضبط)"
-"add" = "أضف"
+"keyboardClosed" = "❌ لوحة المفاتيح مغلقة!"
+"noResult" = "❗ لا يوجد نتائج!"
+"noQuery" = "❌ لم يتم العثور على الاستعلام! يرجى استخدام الأمر مرة أخرى!"
+"wentWrong" = "❌ حدث خطأ ما!"
+"noIpRecord" = "❗ لا يوجد سجل IP!"
+"noInbounds" = "❗ لم يتم العثور على أي وارد!"
+"unlimited" = "♾ غير محدود (إعادة تعيين)"
+"add" = "إضافة"
 "month" = "شهر"
-"months" = "شهور"
+"months" = "أشهر"
 "day" = "يوم"
 "days" = "أيام"
 "hours" = "ساعات"
-"unknown" = "مش معروف"
-"inbounds" = "الإدخالات"
+"minutes" = "دقائق"
+"unknown" = "غير معروف"
+"inbounds" = "الواردات"
 "clients" = "العملاء"
-"offline" = "🔴 أوفلاين"
-"online" = "🟢 أونلاين"
+"offline" = "🔴 غير متصل"
+"online" = "🟢 متصل"
 
 [tgbot.commands]
 "unknown" = "❗ أمر مش معروف."

+ 1 - 0
web/translation/translate.en_US.toml

@@ -574,6 +574,7 @@
 "day" = "Day"
 "days" = "Days"
 "hours" = "Hours"
+"minutes" = "Minutes"
 "unknown" = "Unknown"
 "inbounds" = "Inbounds"
 "clients" = "Clients"

+ 12 - 11
web/translation/translate.es_ES.toml

@@ -91,7 +91,7 @@
 "invalidFormData" = "El formato de los datos de entrada es inválido."
 "emptyUsername" = "Por favor ingresa el nombre de usuario."
 "emptyPassword" = "Por favor ingresa la contraseña."
-"wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto."  
+"wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto."
 "successLogin" = "Has iniciado sesión en tu cuenta correctamente."
 
 [pages.index]
@@ -535,9 +535,9 @@
 
 [pages.settings.security]
 "admin" = "Credenciales de administrador"
-"twoFactor" = "Autenticación de dos factores"  
-"twoFactorEnable" = "Habilitar 2FA"  
-"twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad."  
+"twoFactor" = "Autenticación de dos factores"
+"twoFactorEnable" = "Habilitar 2FA"
+"twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad."
 "twoFactorModalSetTitle" = "Activar autenticación de dos factores"
 "twoFactorModalDeleteTitle" = "Desactivar autenticación de dos factores"
 "twoFactorModalSteps" = "Para configurar la autenticación de dos factores, sigue estos pasos:"
@@ -561,23 +561,24 @@
 "resetOutboundTrafficError" = "Error al reiniciar el tráfico saliente"
 
 [tgbot]
-"keyboardClosed" = "❌ ¡Teclado personalizado cerrado!"
-"noResult" = "❗ ¡Sin resultados!"
-"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor utiliza el comando nuevamente!"
+"keyboardClosed" = "❌ Teclado cerrado!"
+"noResult" = "❗ ¡No hay resultados!"
+"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor, use el comando de nuevo!"
 "wentWrong" = "❌ ¡Algo salió mal!"
-"noIpRecord" = "❗ ¡Sin Registro de IP!"
+"noIpRecord" = "❗ ¡No hay registro de IP!"
 "noInbounds" = "❗ ¡No se encontraron entradas!"
-"unlimited" = "♾ Ilimitado"
-"add" = "Agregar"
+"unlimited" = "♾ Ilimitado (Restablecer)"
+"add" = "Añadir"
 "month" = "Mes"
 "months" = "Meses"
 "day" = "Día"
 "days" = "Días"
 "hours" = "Horas"
+"minutes" = "Minutos"
 "unknown" = "Desconocido"
 "inbounds" = "Entradas"
 "clients" = "Clientes"
-"offline" = "🔴 Sin conexión"
+"offline" = "🔴 Desconectado"
 "online" = "🟢 En línea"
 
 [tgbot.commands]

+ 12 - 11
web/translation/translate.fa_IR.toml

@@ -561,22 +561,23 @@
 "resetOutboundTrafficError" = "خطا در بازنشانی ترافیک خروجی"
 
 [tgbot]
-"keyboardClosed" = "❌ کیبورد سفارشی بسته شد!"
-"noResult" = "❗ نتیجهای یافت نشد!"
-"noQuery" = "❌ کوئری یافت نشد! لطفاً دستور را مجدداً استفاده کنید!"
-"wentWrong" = "❌ مشکلی رخ داده است!"
-"noIpRecord" = "❗ رکورد IP یافت نشد!"
+"keyboardClosed" = "❌ صفحه کلید بسته شد!"
+"noResult" = "❗ نتیجه ای یافت نشد!"
+"noQuery" = "❌ درخواست یافت نشد! لطفا دوباره تلاش کنید!"
+"wentWrong" = "❌ مشکلی پیش آمد!"
+"noIpRecord" = "❗ رکورد آی پی وجود ندارد!"
 "noInbounds" = "❗ هیچ ورودی یافت نشد!"
-"unlimited" = "♾ - نامحدود(ریست)"
-"add" = "اضافه کردن"
+"unlimited" = "♾ نامحدود(ریست)"
+"add" = "افزودن"
 "month" = "ماه"
-"months" = "ماه"
+"months" = "ماه"
 "day" = "روز"
 "days" = "روز"
-"hours" = "ساعت‌"
+"hours" = "ساعت"
+"minutes" = "دقیقه"
 "unknown" = "نامشخص"
-"inbounds" = "ورودیها"
-"clients" = "کلاینت‌ها"
+"inbounds" = "ورودی ها"
+"clients" = "کاربران"
 "offline" = "🔴 آفلاین"
 "online" = "🟢 آنلاین"
 

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

@@ -561,21 +561,22 @@
 "resetOutboundTrafficError" = "Gagal mereset lalu lintas keluar"
 
 [tgbot]
-"keyboardClosed" = "❌ Papan ketik kustom ditutup!"
+"keyboardClosed" = "❌ Keyboard ditutup!"
 "noResult" = "❗ Tidak ada hasil!"
-"noQuery" = "❌ Permintaan tidak ditemukan! Harap gunakan perintah lagi!"
-"wentWrong" = "❌ Ada yang salah!"
+"noQuery" = "❌ Kueri tidak ditemukan! Silakan gunakan perintah lagi!"
+"wentWrong" = "❌ Terjadi kesalahan!"
 "noIpRecord" = "❗ Tidak ada Catatan IP!"
-"noInbounds" = "❗ Tidak ada masuk ditemukan!"
-"unlimited" = "♾ Tak terbatas"
+"noInbounds" = "❗ Tidak ada inbound yang ditemukan!"
+"unlimited" = "♾ Tidak terbatas (Reset)"
 "add" = "Tambah"
 "month" = "Bulan"
 "months" = "Bulan"
 "day" = "Hari"
 "days" = "Hari"
 "hours" = "Jam"
+"minutes" = "Menit"
 "unknown" = "Tidak diketahui"
-"inbounds" = "Masuk"
+"inbounds" = "Inbound"
 "clients" = "Klien"
 "offline" = "🔴 Offline"
 "online" = "🟢 Online"

+ 10 - 9
web/translation/translate.ja_JP.toml

@@ -561,21 +561,22 @@
 "resetOutboundTrafficError" = "送信トラフィックのリセットエラー"
 
 [tgbot]
-"keyboardClosed" = "❌ カスタムキーボードが閉じられました!"
+"keyboardClosed" = "❌ キーボードを閉じました!"
 "noResult" = "❗ 結果がありません!"
-"noQuery" = "❌ クエリが見つかりませんでした!もう一度コマンドを使用してください!"
-"wentWrong" = "❌ 問題が発生しました!"
-"noIpRecord" = "❗ IP記録がありません!"
-"noInbounds" = "❗ インバウンド接続が見つかりません!"
-"unlimited" = "♾ 無制限"
+"noQuery" = "❌ クエリが見つかりません!コマンドを再利用してください!"
+"wentWrong" = "❌ 何かがうまくいかなかった!"
+"noIpRecord" = "❗ IPレコードがありません!"
+"noInbounds" = "❗ インバウンドが見つかりません!"
+"unlimited" = "♾ 無制限(リセット)"
 "add" = "追加"
 "month" = "月"
-"months" = "月"
+"months" = "月"
 "day" = "日"
-"days" = "日"
+"days" = "日"
 "hours" = "時間"
+"minutes" = "分"
 "unknown" = "不明"
-"inbounds" = "インバウンド接続"
+"inbounds" = "インバウンド"
 "clients" = "クライアント"
 "offline" = "🔴 オフライン"
 "online" = "🟢 オンライン"

+ 5 - 4
web/translation/translate.pt_BR.toml

@@ -561,21 +561,22 @@
 "resetOutboundTrafficError" = "Erro ao redefinir tráfego de saída"
 
 [tgbot]
-"keyboardClosed" = "❌ Teclado personalizado fechado!"
+"keyboardClosed" = "❌ Teclado fechado!"
 "noResult" = "❗ Nenhum resultado!"
 "noQuery" = "❌ Consulta não encontrada! Por favor, use o comando novamente!"
 "wentWrong" = "❌ Algo deu errado!"
 "noIpRecord" = "❗ Nenhum registro de IP!"
-"noInbounds" = "❗ Nenhuma entrada encontrada!"
-"unlimited" = "♾ Ilimitado (Reiniciar)"
+"noInbounds" = "❗ Nenhum inbound encontrado!"
+"unlimited" = "♾ Ilimitado (Reset)"
 "add" = "Adicionar"
 "month" = "Mês"
 "months" = "Meses"
 "day" = "Dia"
 "days" = "Dias"
 "hours" = "Horas"
+"minutes" = "Minutos"
 "unknown" = "Desconhecido"
-"inbounds" = "Entradas"
+"inbounds" = "Inbounds"
 "clients" = "Clientes"
 "offline" = "🔴 Offline"
 "online" = "🟢 Online"

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

@@ -574,6 +574,7 @@
 "day" = "День"
 "days" = "Дней"
 "hours" = "Часов"
+"minutes" = "Минуты"
 "unknown" = "Неизвестно"
 "inbounds" = "Инбаунды"
 "clients" = "Клиенты"

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

@@ -561,22 +561,23 @@
 "resetOutboundTrafficError" = "Giden trafik sıfırlanırken hata"
 
 [tgbot]
-"keyboardClosed" = "❌ Özel klavye kapalı!"
+"keyboardClosed" = "❌ Klavye kapatıldı!"
 "noResult" = "❗ Sonuç yok!"
 "noQuery" = "❌ Sorgu bulunamadı! Lütfen komutu tekrar kullanın!"
 "wentWrong" = "❌ Bir şeyler yanlış gitti!"
-"noIpRecord" = "❗ IP Kaydı yok!"
-"noInbounds" = "❗ Gelen bulunamadı!"
-"unlimited" = "♾ Sınırsız(Sıfırla)"
+"noIpRecord" = "❗ IP Kaydı Yok!"
+"noInbounds" = "❗ Gelen bağlantı bulunamadı!"
+"unlimited" = "♾ Sınırsız (Sıfırla)"
 "add" = "Ekle"
 "month" = "Ay"
 "months" = "Aylar"
 "day" = "Gün"
 "days" = "Günler"
 "hours" = "Saatler"
-"unknown" = "Bilinmiyor"
+"minutes" = "Dakika"
+"unknown" = "Bilinmeyen"
 "inbounds" = "Gelenler"
-"clients" = "Müşteriler"
+"clients" = "İstemciler"
 "offline" = "🔴 Çevrimdışı"
 "online" = "🟢 Çevrimiçi"
 

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

@@ -561,19 +561,20 @@
 "resetOutboundTrafficError" = "Помилка скидання вихідного трафіку"
 
 [tgbot]
-"keyboardClosed" = "❌ Спеціальна клавіатура закрита!"
+"keyboardClosed" = "❌ Клавіатуру закрито!"
 "noResult" = "❗ Немає результату!"
-"noQuery" = "❌ Запит не знайдено! Скористайтеся командою ще раз!"
+"noQuery" = "❌ Запит не знайдено! Будь ласка, використовуйте команду ще раз!"
 "wentWrong" = "❌ Щось пішло не так!"
-"noIpRecord" = "❗ Немає IP-запису!"
-"noInbounds" = "❗ Вхідних не знайдено!"
-"unlimited" = "♾ Необмежений (скинути)"
+"noIpRecord" = "❗ Немає запису IP!"
+"noInbounds" = "❗ Вхідні не знайдені!"
+"unlimited" = "♾ Необмежено (Скинути)"
 "add" = "Додати"
 "month" = "Місяць"
 "months" = "Місяці"
 "day" = "День"
 "days" = "Дні"
-"hours" = "Годинник"
+"hours" = "Години"
+"minutes" = "Хвилини"
 "unknown" = "Невідомо"
 "inbounds" = "Вхідні"
 "clients" = "Клієнти"

+ 11 - 10
web/translation/translate.vi_VN.toml

@@ -91,7 +91,7 @@
 "invalidFormData" = "Dạng dữ liệu nhập không hợp lệ."
 "emptyUsername" = "Vui lòng nhập tên người dùng."
 "emptyPassword" = "Vui lòng nhập mật khẩu."
-"wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ."  
+"wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ."
 "successLogin" = "Bạn đã đăng nhập vào tài khoản thành công."
 
 [pages.index]
@@ -535,9 +535,9 @@
 
 [pages.settings.security]
 "admin" = "Thông tin đăng nhập quản trị viên"
-"twoFactor" = "Xác thực hai yếu tố"  
-"twoFactorEnable" = "Bật 2FA"  
-"twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn."  
+"twoFactor" = "Xác thực hai yếu tố"
+"twoFactorEnable" = "Bật 2FA"
+"twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn."
 "twoFactorModalSetTitle" = "Bật xác thực hai yếu tố"
 "twoFactorModalDeleteTitle" = "Tắt xác thực hai yếu tố"
 "twoFactorModalSteps" = "Để thiết lập xác thực hai yếu tố, hãy thực hiện các bước sau:"
@@ -561,22 +561,23 @@
 "resetOutboundTrafficError" = "Lỗi khi đặt lại lưu lượng truy cập đi"
 
 [tgbot]
-"keyboardClosed" = "❌ Bàn phím tùy chỉnh đã đóng!"
+"keyboardClosed" = "❌ Bàn phím đã đóng!"
 "noResult" = "❗ Không có kết quả!"
-"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lệnh lại!"
+"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lại lệnh!"
 "wentWrong" = "❌ Đã xảy ra lỗi!"
 "noIpRecord" = "❗ Không có bản ghi IP!"
 "noInbounds" = "❗ Không tìm thấy inbound!"
-"unlimited" = "♾ Không giới hạn"
+"unlimited" = "♾ Không giới hạn (Đặt lại)"
 "add" = "Thêm"
 "month" = "Tháng"
 "months" = "Tháng"
 "day" = "Ngày"
 "days" = "Ngày"
 "hours" = "Giờ"
-"unknown" = "Không rõ"
-"inbounds" = "Vào"
-"clients" = "Các người dùng"
+"minutes" = "Phút"
+"unknown" = "Không xác định"
+"inbounds" = "Inbound"
+"clients" = "Client"
 "offline" = "🔴 Ngoại tuyến"
 "online" = "🟢 Trực tuyến"
 

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

@@ -563,19 +563,20 @@
 [tgbot]
 "keyboardClosed" = "❌ 自定义键盘已关闭!"
 "noResult" = "❗ 没有结果!"
-"noQuery" = "❌ 未找到查询!请重新使用命令!"
+"noQuery" = "❌ 未找到查询!请再次使用该命令!"
 "wentWrong" = "❌ 出了点问题!"
-"noIpRecord" = "❗ 没有 IP 记录!"
-"noInbounds" = "❗ 没有找到入站连接!"
-"unlimited" = "♾ 无限"
+"noIpRecord" = "❗ 没有IP记录!"
+"noInbounds" = "❗ 未找到入站!"
+"unlimited" = "♾ 无限(重置)"
 "add" = "添加"
 "month" = "月"
 "months" = "月"
 "day" = "天"
 "days" = "天"
 "hours" = "小时"
+"minutes" = "分钟"
 "unknown" = "未知"
-"inbounds" = "入站连接"
+"inbounds" = "入站"
 "clients" = "客户端"
 "offline" = "🔴 离线"
 "online" = "🟢 在线"

+ 8 - 7
web/translation/translate.zh_TW.toml

@@ -563,22 +563,23 @@
 [tgbot]
 "keyboardClosed" = "❌ 自定義鍵盤已關閉!"
 "noResult" = "❗ 沒有結果!"
-"noQuery" = "❌ 未找到查詢!請重新使用命令!"
+"noQuery" = "❌ 未找到查詢!請再次使用該命令!"
 "wentWrong" = "❌ 出了點問題!"
-"noIpRecord" = "❗ 沒有 IP 記錄!"
-"noInbounds" = "❗ 沒有找到入站連線!"
-"unlimited" = "♾ 無限"
-"add" = "新增"
+"noIpRecord" = "❗ 沒有IP記錄!"
+"noInbounds" = "❗ 未找到入站!"
+"unlimited" = "♾ 無限(重置)"
+"add" = "添加"
 "month" = "月"
 "months" = "月"
 "day" = "天"
 "days" = "天"
 "hours" = "小時"
+"minutes" = "分鐘"
 "unknown" = "未知"
-"inbounds" = "入站連線"
+"inbounds" = "入站"
 "clients" = "客戶端"
 "offline" = "🔴 離線"
-"online" = "🟢 線"
+"online" = "🟢 線"
 
 [tgbot.commands]
 "unknown" = "❗ 未知命令"