diff --git a/database/db.go b/database/db.go index 78446ca3..58556e80 100644 --- a/database/db.go +++ b/database/db.go @@ -69,11 +69,11 @@ func isIgnorableDuplicateColumnErr(err error, mdl any) bool { if !strings.Contains(errMsg, dupPrefix) { return false } - idx := strings.Index(errMsg, dupPrefix) - if idx < 0 { + _, after, ok := strings.Cut(errMsg, dupPrefix) + if !ok { return false } - col := strings.TrimSpace(errMsg[idx+len(dupPrefix):]) + col := strings.TrimSpace(after) col = strings.Trim(col, "`\"[]") if col == "" { return false diff --git a/go.mod b/go.mod index b3c988d0..6eb1e5c7 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/joho/godotenv v1.5.1 - github.com/mymmrac/telego v1.8.0 + github.com/mymmrac/telego v1.9.0 github.com/nicksnyder/go-i18n/v2 v2.6.1 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/robfig/cron/v3 v3.0.1 @@ -25,7 +25,7 @@ require ( golang.org/x/crypto v0.51.0 golang.org/x/sys v0.44.0 golang.org/x/text v0.37.0 - google.golang.org/grpc v1.81.0 + google.golang.org/grpc v1.81.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 diff --git a/go.sum b/go.sum index f4dfd091..7d03ead2 100644 --- a/go.sum +++ b/go.sum @@ -130,8 +130,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mymmrac/telego v1.8.0 h1:EvIprWo9Cn0MHgumvvqNXPAXO1yJj3pu2cdCCeDxbow= -github.com/mymmrac/telego v1.8.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM= +github.com/mymmrac/telego v1.9.0 h1:ZUJxZaPx/1IgRvVb5lXnUB8FgW5rNYfRe6Q2EJ4OJ+Y= +github.com/mymmrac/telego v1.9.0/go.mod h1:tVEB7OqiOPx8elRk9+ETkwiDQrUhWSB2XmAKIY9KmWY= github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= @@ -258,8 +258,8 @@ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= -google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/sub/sub.go b/sub/sub.go index eb3fece7..534af5ff 100644 --- a/sub/sub.go +++ b/sub/sub.go @@ -207,9 +207,9 @@ func (s *Server) initRouter() (*gin.Engine, error) { path := c.Request.URL.Path pathPrefix := strings.TrimRight(LinksPath, "/") + "/" if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") { - assetsIndex := strings.Index(path, "/assets/") - if assetsIndex != -1 { - assetPath := path[assetsIndex+8:] // +8 to skip "/assets/" + _, after, ok := strings.Cut(path, "/assets/") + if ok { + assetPath := after // +8 to skip "/assets/" if assetPath != "" { c.FileFromFS(assetPath, assetsFS) c.Abort() diff --git a/sub/subClashService.go b/sub/subClashService.go index c94ea467..74bcad94 100644 --- a/sub/subClashService.go +++ b/sub/subClashService.go @@ -2,6 +2,7 @@ package sub import ( "fmt" + "maps" "strings" "github.com/goccy/go-json" @@ -471,8 +472,6 @@ func cloneMap(src map[string]any) map[string]any { return nil } dst := make(map[string]any, len(src)) - for k, v := range src { - dst[k] = v - } + maps.Copy(dst, src) return dst } diff --git a/util/ldap/ldap.go b/util/ldap/ldap.go index 1b9faa53..4d5bdbb7 100644 --- a/util/ldap/ldap.go +++ b/util/ldap/ldap.go @@ -3,6 +3,7 @@ package ldaputil import ( "crypto/tls" "fmt" + "slices" "github.com/go-ldap/ldap/v3" ) @@ -82,13 +83,7 @@ func FetchVlessFlags(cfg Config) (map[string]bool, error) { continue } val := e.GetAttributeValue(cfg.FlagField) - enabled := false - for _, t := range cfg.TruthyVals { - if val == t { - enabled = true - break - } - } + enabled := slices.Contains(cfg.TruthyVals, val) if cfg.Invert { enabled = !enabled } diff --git a/util/random/random_test.go b/util/random/random_test.go index 5c33f6d7..57eb3c59 100644 --- a/util/random/random_test.go +++ b/util/random/random_test.go @@ -32,7 +32,7 @@ func TestSeq_NotConstant(t *testing.T) { func TestNum_InRange(t *testing.T) { for _, upper := range []int{1, 2, 10, 1000} { - for i := 0; i < 200; i++ { + for range 200 { v := Num(upper) if v < 0 || v >= upper { t.Fatalf("Num(%d) returned %d, out of [0, %d)", upper, v, upper) diff --git a/util/sys/sys_darwin.go b/util/sys/sys_darwin.go index b44d7689..b62b5f7b 100644 --- a/util/sys/sys_darwin.go +++ b/util/sys/sys_darwin.go @@ -1,5 +1,4 @@ //go:build darwin -// +build darwin package sys diff --git a/util/sys/sys_linux.go b/util/sys/sys_linux.go index 5b1b1127..00d02a59 100644 --- a/util/sys/sys_linux.go +++ b/util/sys/sys_linux.go @@ -1,5 +1,4 @@ //go:build linux -// +build linux package sys diff --git a/util/sys/sys_windows.go b/util/sys/sys_windows.go index 9b6d659f..008a0466 100644 --- a/util/sys/sys_windows.go +++ b/util/sys/sys_windows.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package sys diff --git a/web/controller/api.go b/web/controller/api.go index b7ac15c1..572410e9 100644 --- a/web/controller/api.go +++ b/web/controller/api.go @@ -32,8 +32,8 @@ func NewAPIController(g *gin.RouterGroup, customGeo *service.CustomGeoService) * func (a *APIController) checkAPIAuth(c *gin.Context) { auth := c.GetHeader("Authorization") - if strings.HasPrefix(auth, "Bearer ") { - tok := strings.TrimPrefix(auth, "Bearer ") + if after, ok := strings.CutPrefix(auth, "Bearer "); ok { + tok := after if a.apiTokenService.Match(tok) { if u, err := a.userService.GetFirstUser(); err == nil { session.SetAPIAuthUser(c, u) diff --git a/web/controller/util.go b/web/controller/util.go index 94e17513..7d77f580 100644 --- a/web/controller/util.go +++ b/web/controller/util.go @@ -27,7 +27,7 @@ func getRemoteIp(c *gin.Context) string { } if xff := c.GetHeader("X-Forwarded-For"); xff != "" { - for _, part := range strings.Split(xff, ",") { + for part := range strings.SplitSeq(xff, ",") { if ip, ok := extractTrustedIP(part); ok { return ip } @@ -50,7 +50,7 @@ func isTrustedProxy(ip string) bool { } trusted := trustedProxyCIDRs() - for _, value := range strings.Split(trusted, ",") { + for value := range strings.SplitSeq(trusted, ",") { value = strings.TrimSpace(value) if value == "" { continue diff --git a/web/entity/entity.go b/web/entity/entity.go index bc4ce5a1..82c33d10 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -195,7 +195,7 @@ func (s *AllSetting) CheckValid() error { s.SubClashPath += "/" } - for _, cidr := range strings.Split(s.TrustedProxyCIDRs, ",") { + for cidr := range strings.SplitSeq(s.TrustedProxyCIDRs, ",") { cidr = strings.TrimSpace(cidr) if cidr == "" { continue diff --git a/web/job/node_traffic_sync_job_test.go b/web/job/node_traffic_sync_job_test.go index 1bc7601e..ec04e350 100644 --- a/web/job/node_traffic_sync_job_test.go +++ b/web/job/node_traffic_sync_job_test.go @@ -42,28 +42,24 @@ func TestAtomicBool_ConcurrentSettersExactlyOneTakeWins(t *testing.T) { const readers = 20 var wg sync.WaitGroup - for i := 0; i < setters; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range setters { + wg.Go(func() { a.set() - }() + }) } wg.Wait() trueCount := 0 var rwg sync.WaitGroup var mu sync.Mutex - for i := 0; i < readers; i++ { - rwg.Add(1) - go func() { - defer rwg.Done() + for range readers { + rwg.Go(func() { if a.takeAndReset() { mu.Lock() trueCount++ mu.Unlock() } - }() + }) } rwg.Wait() diff --git a/web/service/inbound.go b/web/service/inbound.go index 950cd74a..17e2d191 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -2526,10 +2526,7 @@ func chunkStrings(s []string, size int) [][]string { } out := make([][]string, 0, (len(s)+size-1)/size) for i := 0; i < len(s); i += size { - end := i + size - if end > len(s) { - end = len(s) - } + end := min(i+size, len(s)) out = append(out, s[i:end]) } return out @@ -2543,10 +2540,7 @@ func chunkInts(s []int, size int) [][]int { } out := make([][]int, 0, (len(s)+size-1)/size) for i := 0; i < len(s); i += size { - end := i + size - if end > len(s) { - end = len(s) - } + end := min(i+size, len(s)) out = append(out, s[i:end]) } return out diff --git a/web/service/panel.go b/web/service/panel.go index 3ab51ab3..a5480677 100644 --- a/web/service/panel.go +++ b/web/service/panel.go @@ -213,7 +213,7 @@ func compareVersionStrings(a string, b string) (int, bool) { if !okA || !okB { return 0, false } - for i := 0; i < len(aParts); i++ { + for i := range len(aParts) { if aParts[i] > bParts[i] { return 1, true } diff --git a/web/service/port_conflict.go b/web/service/port_conflict.go index a2dd2183..8d71082b 100644 --- a/web/service/port_conflict.go +++ b/web/service/port_conflict.go @@ -72,7 +72,7 @@ func inboundTransports(protocol model.Protocol, streamSettings, settings string) // "udp", or "tcp,udp". if it's set, it wins outright. if n, ok := st["network"].(string); ok && n != "" { bits = 0 - for _, part := range strings.Split(n, ",") { + for part := range strings.SplitSeq(n, ",") { switch strings.TrimSpace(part) { case "tcp": bits |= transportTCP diff --git a/web/service/port_conflict_test.go b/web/service/port_conflict_test.go index 1a7f0c1e..70f637d9 100644 --- a/web/service/port_conflict_test.go +++ b/web/service/port_conflict_test.go @@ -56,7 +56,8 @@ func seedInboundConflictNode(t *testing.T, tag, listen string, port int, protoco } } -func intPtr(v int) *int { return &v } +//go:fix inline +func intPtr(v int) *int { return new(v) } func TestInboundTransports(t *testing.T) { cases := []struct { @@ -360,7 +361,7 @@ func TestGenerateInboundTag_SpecificListenSameDisambiguation(t *testing.T) { func TestCheckPortConflict_NodeScope(t *testing.T) { setupConflictDB(t) seedInboundConflictNode(t, "local-443-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{}`, nil) - seedInboundConflictNode(t, "node1-443-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{}`, intPtr(1)) + seedInboundConflictNode(t, "node1-443-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{}`, new(1)) svc := &InboundService{} @@ -370,8 +371,8 @@ func TestCheckPortConflict_NodeScope(t *testing.T) { want bool }{ {"new local same port + tcp clashes with local", nil, true}, - {"new remote on different node from local is fine", intPtr(2), false}, - {"new remote on existing node 1 clashes", intPtr(1), true}, + {"new remote on different node from local is fine", new(2), false}, + {"new remote on existing node 1 clashes", new(1), true}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 426729e8..d62f23a7 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -227,7 +227,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error { parsedAdminIds := make([]int64, 0) // Parse admin IDs from comma-separated string if tgBotID != "" { - for _, adminID := range strings.Split(tgBotID, ",") { + for adminID := range strings.SplitSeq(tgBotID, ",") { id, err := strconv.ParseInt(adminID, 10, 64) if err != nil { logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err) @@ -2051,12 +2051,7 @@ func (t *Tgbot) SubmitAddClient() (bool, error) { // checkAdmin checks if the given Telegram ID is an admin. func checkAdmin(tgId int64) bool { - for _, adminId := range adminIds { - if adminId == tgId { - return true - } - } - return false + return slices.Contains(adminIds, tgId) } // SendAnswer sends a response message with an inline keyboard to the specified chat. @@ -2373,17 +2368,18 @@ func (t *Tgbot) sendClientIndividualLinks(chatId int64, email string) { // Send in chunks to respect message length; use monospace formatting const maxPerMessage = 50 for i := 0; i < len(cleaned); i += maxPerMessage { - j := i + maxPerMessage - if j > len(cleaned) { - j = len(cleaned) - } + j := min(i+maxPerMessage, len(cleaned)) chunk := cleaned[i:j] - msg := t.I18nBot("subscription.individualLinks") + ":\r\n" + var msg strings.Builder + msg.WriteString(t.I18nBot("subscription.individualLinks")) + msg.WriteString(":\r\n") for _, link := range chunk { // wrap each link in - msg += "" + link + "\r\n" + msg.WriteString("") + msg.WriteString(link) + msg.WriteString("\r\n") } - t.SendMsgToTgbot(chatId, msg) + t.SendMsgToTgbot(chatId, msg.String()) } } @@ -3439,7 +3435,8 @@ func (t *Tgbot) notifyExhausted() { var exhaustedClients []xray.ClientTraffic traffics, err := t.inboundService.GetClientTrafficTgBot(client.TgID) if err == nil && len(traffics) > 0 { - output := t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients")) + var output strings.Builder + output.WriteString(t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients"))) for _, traffic := range traffics { if traffic.Enable { if (traffic.ExpiryTime > 0 && (traffic.ExpiryTime-now < exDiff)) || @@ -3451,21 +3448,23 @@ func (t *Tgbot) notifyExhausted() { } } if len(exhaustedClients) > 0 { - output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients))) + output.WriteString(t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients)))) if len(disabledClients) > 0 { - output += t.I18nBot("tgbot.clients") + ":\r\n" + output.WriteString(t.I18nBot("tgbot.clients")) + output.WriteString(":\r\n") for _, traffic := range disabledClients { - output += " " + traffic.Email + output.WriteString(" ") + output.WriteString(traffic.Email) } - output += "\r\n" + output.WriteString("\r\n") } - output += "\r\n" - output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedClients))) + output.WriteString("\r\n") + output.WriteString(t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedClients)))) for _, traffic := range exhaustedClients { - output += t.clientInfoMsg(&traffic, true, false, false, true, true, false) - output += "\r\n" + output.WriteString(t.clientInfoMsg(&traffic, true, false, false, true, true, false)) + output.WriteString("\r\n") } - t.SendMsgToTgbot(chatID, output) + t.SendMsgToTgbot(chatID, output.String()) } chatIDsDone = append(chatIDsDone, chatID) } @@ -3480,12 +3479,7 @@ func (t *Tgbot) notifyExhausted() { // int64Contains checks if an int64 slice contains a specific item. func int64Contains(slice []int64, item int64) bool { - for _, s := range slice { - if s == item { - return true - } - } - return false + return slices.Contains(slice, item) } // onlineClients retrieves and sends information about online clients. diff --git a/web/service/tgbot_test.go b/web/service/tgbot_test.go index 39173563..70411122 100644 --- a/web/service/tgbot_test.go +++ b/web/service/tgbot_test.go @@ -6,7 +6,7 @@ import ( ) func TestLoginAttemptDoesNotCarryPassword(t *testing.T) { - typ := reflect.TypeOf(LoginAttempt{}) + typ := reflect.TypeFor[LoginAttempt]() if _, ok := typ.FieldByName("Password"); ok { t.Fatal("LoginAttempt must not carry attempted passwords") } diff --git a/web/service/xray_setting.go b/web/service/xray_setting.go index 4249f018..1fda04aa 100644 --- a/web/service/xray_setting.go +++ b/web/service/xray_setting.go @@ -3,6 +3,7 @@ package service import ( _ "embed" "encoding/json" + "slices" "github.com/mhsanaei/3x-ui/v3/util/common" "github.com/mhsanaei/3x-ui/v3/xray" @@ -55,7 +56,7 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error { // If `raw` does not look like a wrapper, it is returned unchanged. func UnwrapXrayTemplateConfig(raw string) string { const maxDepth = 8 // defensive cap against pathological multi-nest values - for i := 0; i < maxDepth; i++ { + for range maxDepth { var top map[string]json.RawMessage if err := json.Unmarshal([]byte(raw), &top); err != nil { return raw @@ -190,10 +191,8 @@ func findApiRule(rules []map[string]any) int { } } case []string: - for _, s := range tags { - if s == "api" { - return i - } + if slices.Contains(tags, "api") { + return i } case string: if tags == "api" { diff --git a/web/service/xray_setting_test.go b/web/service/xray_setting_test.go index 22b00ce3..5fe54df7 100644 --- a/web/service/xray_setting_test.go +++ b/web/service/xray_setting_test.go @@ -65,7 +65,7 @@ func TestUnwrapXrayTemplateConfig(t *testing.T) { // non-wrapped, and confirm we end up at some valid JSON (we // don't loop forever and we don't blow the stack). s := real - for i := 0; i < 16; i++ { + for range 16 { s = `{"xraySetting":` + s + `}` } got := UnwrapXrayTemplateConfig(s) diff --git a/web/websocket/hub.go b/web/websocket/hub.go index 6df470d9..2b8773cf 100644 --- a/web/websocket/hub.go +++ b/web/websocket/hub.go @@ -110,7 +110,7 @@ func (h *Hub) shouldThrottle(msgType MessageType) bool { // panic doesn't permanently kill real-time updates for commercial deployments. // After the cap, the hub stays down and the frontend falls back to REST polling. func (h *Hub) Run() { - for attempt := 0; attempt < hubRestartAttempts; attempt++ { + for attempt := range hubRestartAttempts { stopped := h.runOnce() if stopped { return diff --git a/web/websocket/hub_test.go b/web/websocket/hub_test.go index 2a418068..18998789 100644 --- a/web/websocket/hub_test.go +++ b/web/websocket/hub_test.go @@ -231,7 +231,7 @@ func TestHub_ConcurrentRegisterUnregister(t *testing.T) { const n = 50 var wg sync.WaitGroup - for i := 0; i < n; i++ { + for i := range n { wg.Add(1) go func(idx int) { defer wg.Done()