mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-08 06:04:10 +00:00
chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
This commit is contained in:
parent
317c7cb4f1
commit
1ef494bcda
24 changed files with 69 additions and 94 deletions
|
|
@ -69,11 +69,11 @@ func isIgnorableDuplicateColumnErr(err error, mdl any) bool {
|
||||||
if !strings.Contains(errMsg, dupPrefix) {
|
if !strings.Contains(errMsg, dupPrefix) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
idx := strings.Index(errMsg, dupPrefix)
|
_, after, ok := strings.Cut(errMsg, dupPrefix)
|
||||||
if idx < 0 {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
col := strings.TrimSpace(errMsg[idx+len(dupPrefix):])
|
col := strings.TrimSpace(after)
|
||||||
col = strings.Trim(col, "`\"[]")
|
col = strings.Trim(col, "`\"[]")
|
||||||
if col == "" {
|
if col == "" {
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
4
go.mod
4
go.mod
|
|
@ -12,7 +12,7 @@ require (
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/joho/godotenv v1.5.1
|
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/nicksnyder/go-i18n/v2 v2.6.1
|
||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
|
|
@ -25,7 +25,7 @@ require (
|
||||||
golang.org/x/crypto v0.51.0
|
golang.org/x/crypto v0.51.0
|
||||||
golang.org/x/sys v0.44.0
|
golang.org/x/sys v0.44.0
|
||||||
golang.org/x/text v0.37.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
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gorm.io/driver/sqlite v1.6.0
|
gorm.io/driver/sqlite v1.6.0
|
||||||
gorm.io/gorm v1.31.1
|
gorm.io/gorm v1.31.1
|
||||||
|
|
|
||||||
8
go.sum
8
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/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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
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.9.0 h1:ZUJxZaPx/1IgRvVb5lXnUB8FgW5rNYfRe6Q2EJ4OJ+Y=
|
||||||
github.com/mymmrac/telego v1.8.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM=
|
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 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
|
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=
|
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=
|
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 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
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.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||||
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
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 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
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=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|
|
||||||
|
|
@ -207,9 +207,9 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
path := c.Request.URL.Path
|
path := c.Request.URL.Path
|
||||||
pathPrefix := strings.TrimRight(LinksPath, "/") + "/"
|
pathPrefix := strings.TrimRight(LinksPath, "/") + "/"
|
||||||
if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") {
|
if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") {
|
||||||
assetsIndex := strings.Index(path, "/assets/")
|
_, after, ok := strings.Cut(path, "/assets/")
|
||||||
if assetsIndex != -1 {
|
if ok {
|
||||||
assetPath := path[assetsIndex+8:] // +8 to skip "/assets/"
|
assetPath := after // +8 to skip "/assets/"
|
||||||
if assetPath != "" {
|
if assetPath != "" {
|
||||||
c.FileFromFS(assetPath, assetsFS)
|
c.FileFromFS(assetPath, assetsFS)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package sub
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
|
|
@ -471,8 +472,6 @@ func cloneMap(src map[string]any) map[string]any {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
dst := make(map[string]any, len(src))
|
dst := make(map[string]any, len(src))
|
||||||
for k, v := range src {
|
maps.Copy(dst, src)
|
||||||
dst[k] = v
|
|
||||||
}
|
|
||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package ldaputil
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
)
|
)
|
||||||
|
|
@ -82,13 +83,7 @@ func FetchVlessFlags(cfg Config) (map[string]bool, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
val := e.GetAttributeValue(cfg.FlagField)
|
val := e.GetAttributeValue(cfg.FlagField)
|
||||||
enabled := false
|
enabled := slices.Contains(cfg.TruthyVals, val)
|
||||||
for _, t := range cfg.TruthyVals {
|
|
||||||
if val == t {
|
|
||||||
enabled = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cfg.Invert {
|
if cfg.Invert {
|
||||||
enabled = !enabled
|
enabled = !enabled
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ func TestSeq_NotConstant(t *testing.T) {
|
||||||
|
|
||||||
func TestNum_InRange(t *testing.T) {
|
func TestNum_InRange(t *testing.T) {
|
||||||
for _, upper := range []int{1, 2, 10, 1000} {
|
for _, upper := range []int{1, 2, 10, 1000} {
|
||||||
for i := 0; i < 200; i++ {
|
for range 200 {
|
||||||
v := Num(upper)
|
v := Num(upper)
|
||||||
if v < 0 || v >= upper {
|
if v < 0 || v >= upper {
|
||||||
t.Fatalf("Num(%d) returned %d, out of [0, %d)", upper, v, upper)
|
t.Fatalf("Num(%d) returned %d, out of [0, %d)", upper, v, upper)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build darwin
|
//go:build darwin
|
||||||
// +build darwin
|
|
||||||
|
|
||||||
package sys
|
package sys
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build linux
|
//go:build linux
|
||||||
// +build linux
|
|
||||||
|
|
||||||
package sys
|
package sys
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build windows
|
//go:build windows
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package sys
|
package sys
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ func NewAPIController(g *gin.RouterGroup, customGeo *service.CustomGeoService) *
|
||||||
|
|
||||||
func (a *APIController) checkAPIAuth(c *gin.Context) {
|
func (a *APIController) checkAPIAuth(c *gin.Context) {
|
||||||
auth := c.GetHeader("Authorization")
|
auth := c.GetHeader("Authorization")
|
||||||
if strings.HasPrefix(auth, "Bearer ") {
|
if after, ok := strings.CutPrefix(auth, "Bearer "); ok {
|
||||||
tok := strings.TrimPrefix(auth, "Bearer ")
|
tok := after
|
||||||
if a.apiTokenService.Match(tok) {
|
if a.apiTokenService.Match(tok) {
|
||||||
if u, err := a.userService.GetFirstUser(); err == nil {
|
if u, err := a.userService.GetFirstUser(); err == nil {
|
||||||
session.SetAPIAuthUser(c, u)
|
session.SetAPIAuthUser(c, u)
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ func getRemoteIp(c *gin.Context) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
if xff := c.GetHeader("X-Forwarded-For"); xff != "" {
|
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 {
|
if ip, ok := extractTrustedIP(part); ok {
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +50,7 @@ func isTrustedProxy(ip string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
trusted := trustedProxyCIDRs()
|
trusted := trustedProxyCIDRs()
|
||||||
for _, value := range strings.Split(trusted, ",") {
|
for value := range strings.SplitSeq(trusted, ",") {
|
||||||
value = strings.TrimSpace(value)
|
value = strings.TrimSpace(value)
|
||||||
if value == "" {
|
if value == "" {
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ func (s *AllSetting) CheckValid() error {
|
||||||
s.SubClashPath += "/"
|
s.SubClashPath += "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cidr := range strings.Split(s.TrustedProxyCIDRs, ",") {
|
for cidr := range strings.SplitSeq(s.TrustedProxyCIDRs, ",") {
|
||||||
cidr = strings.TrimSpace(cidr)
|
cidr = strings.TrimSpace(cidr)
|
||||||
if cidr == "" {
|
if cidr == "" {
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -42,28 +42,24 @@ func TestAtomicBool_ConcurrentSettersExactlyOneTakeWins(t *testing.T) {
|
||||||
const readers = 20
|
const readers = 20
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
for i := 0; i < setters; i++ {
|
for range setters {
|
||||||
wg.Add(1)
|
wg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
a.set()
|
a.set()
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
trueCount := 0
|
trueCount := 0
|
||||||
var rwg sync.WaitGroup
|
var rwg sync.WaitGroup
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
for i := 0; i < readers; i++ {
|
for range readers {
|
||||||
rwg.Add(1)
|
rwg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer rwg.Done()
|
|
||||||
if a.takeAndReset() {
|
if a.takeAndReset() {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
trueCount++
|
trueCount++
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
rwg.Wait()
|
rwg.Wait()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2526,10 +2526,7 @@ func chunkStrings(s []string, size int) [][]string {
|
||||||
}
|
}
|
||||||
out := make([][]string, 0, (len(s)+size-1)/size)
|
out := make([][]string, 0, (len(s)+size-1)/size)
|
||||||
for i := 0; i < len(s); i += size {
|
for i := 0; i < len(s); i += size {
|
||||||
end := i + size
|
end := min(i+size, len(s))
|
||||||
if end > len(s) {
|
|
||||||
end = len(s)
|
|
||||||
}
|
|
||||||
out = append(out, s[i:end])
|
out = append(out, s[i:end])
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|
@ -2543,10 +2540,7 @@ func chunkInts(s []int, size int) [][]int {
|
||||||
}
|
}
|
||||||
out := make([][]int, 0, (len(s)+size-1)/size)
|
out := make([][]int, 0, (len(s)+size-1)/size)
|
||||||
for i := 0; i < len(s); i += size {
|
for i := 0; i < len(s); i += size {
|
||||||
end := i + size
|
end := min(i+size, len(s))
|
||||||
if end > len(s) {
|
|
||||||
end = len(s)
|
|
||||||
}
|
|
||||||
out = append(out, s[i:end])
|
out = append(out, s[i:end])
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ func compareVersionStrings(a string, b string) (int, bool) {
|
||||||
if !okA || !okB {
|
if !okA || !okB {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
for i := 0; i < len(aParts); i++ {
|
for i := range len(aParts) {
|
||||||
if aParts[i] > bParts[i] {
|
if aParts[i] > bParts[i] {
|
||||||
return 1, true
|
return 1, true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ func inboundTransports(protocol model.Protocol, streamSettings, settings string)
|
||||||
// "udp", or "tcp,udp". if it's set, it wins outright.
|
// "udp", or "tcp,udp". if it's set, it wins outright.
|
||||||
if n, ok := st["network"].(string); ok && n != "" {
|
if n, ok := st["network"].(string); ok && n != "" {
|
||||||
bits = 0
|
bits = 0
|
||||||
for _, part := range strings.Split(n, ",") {
|
for part := range strings.SplitSeq(n, ",") {
|
||||||
switch strings.TrimSpace(part) {
|
switch strings.TrimSpace(part) {
|
||||||
case "tcp":
|
case "tcp":
|
||||||
bits |= transportTCP
|
bits |= transportTCP
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
func TestInboundTransports(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
|
|
@ -360,7 +361,7 @@ func TestGenerateInboundTag_SpecificListenSameDisambiguation(t *testing.T) {
|
||||||
func TestCheckPortConflict_NodeScope(t *testing.T) {
|
func TestCheckPortConflict_NodeScope(t *testing.T) {
|
||||||
setupConflictDB(t)
|
setupConflictDB(t)
|
||||||
seedInboundConflictNode(t, "local-443-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{}`, nil)
|
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{}
|
svc := &InboundService{}
|
||||||
|
|
||||||
|
|
@ -370,8 +371,8 @@ func TestCheckPortConflict_NodeScope(t *testing.T) {
|
||||||
want bool
|
want bool
|
||||||
}{
|
}{
|
||||||
{"new local same port + tcp clashes with local", nil, true},
|
{"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 different node from local is fine", new(2), false},
|
||||||
{"new remote on existing node 1 clashes", intPtr(1), true},
|
{"new remote on existing node 1 clashes", new(1), true},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
parsedAdminIds := make([]int64, 0)
|
parsedAdminIds := make([]int64, 0)
|
||||||
// Parse admin IDs from comma-separated string
|
// Parse admin IDs from comma-separated string
|
||||||
if tgBotID != "" {
|
if tgBotID != "" {
|
||||||
for _, adminID := range strings.Split(tgBotID, ",") {
|
for adminID := range strings.SplitSeq(tgBotID, ",") {
|
||||||
id, err := strconv.ParseInt(adminID, 10, 64)
|
id, err := strconv.ParseInt(adminID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err)
|
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.
|
// checkAdmin checks if the given Telegram ID is an admin.
|
||||||
func checkAdmin(tgId int64) bool {
|
func checkAdmin(tgId int64) bool {
|
||||||
for _, adminId := range adminIds {
|
return slices.Contains(adminIds, tgId)
|
||||||
if adminId == tgId {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendAnswer sends a response message with an inline keyboard to the specified chat.
|
// 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
|
// Send in chunks to respect message length; use monospace formatting
|
||||||
const maxPerMessage = 50
|
const maxPerMessage = 50
|
||||||
for i := 0; i < len(cleaned); i += maxPerMessage {
|
for i := 0; i < len(cleaned); i += maxPerMessage {
|
||||||
j := i + maxPerMessage
|
j := min(i+maxPerMessage, len(cleaned))
|
||||||
if j > len(cleaned) {
|
|
||||||
j = len(cleaned)
|
|
||||||
}
|
|
||||||
chunk := cleaned[i:j]
|
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 {
|
for _, link := range chunk {
|
||||||
// wrap each link in <code>
|
// wrap each link in <code>
|
||||||
msg += "<code>" + link + "</code>\r\n"
|
msg.WriteString("<code>")
|
||||||
|
msg.WriteString(link)
|
||||||
|
msg.WriteString("</code>\r\n")
|
||||||
}
|
}
|
||||||
t.SendMsgToTgbot(chatId, msg)
|
t.SendMsgToTgbot(chatId, msg.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3439,7 +3435,8 @@ func (t *Tgbot) notifyExhausted() {
|
||||||
var exhaustedClients []xray.ClientTraffic
|
var exhaustedClients []xray.ClientTraffic
|
||||||
traffics, err := t.inboundService.GetClientTrafficTgBot(client.TgID)
|
traffics, err := t.inboundService.GetClientTrafficTgBot(client.TgID)
|
||||||
if err == nil && len(traffics) > 0 {
|
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 {
|
for _, traffic := range traffics {
|
||||||
if traffic.Enable {
|
if traffic.Enable {
|
||||||
if (traffic.ExpiryTime > 0 && (traffic.ExpiryTime-now < exDiff)) ||
|
if (traffic.ExpiryTime > 0 && (traffic.ExpiryTime-now < exDiff)) ||
|
||||||
|
|
@ -3451,21 +3448,23 @@ func (t *Tgbot) notifyExhausted() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(exhaustedClients) > 0 {
|
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 {
|
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 {
|
for _, traffic := range disabledClients {
|
||||||
output += " " + traffic.Email
|
output.WriteString(" ")
|
||||||
|
output.WriteString(traffic.Email)
|
||||||
}
|
}
|
||||||
output += "\r\n"
|
output.WriteString("\r\n")
|
||||||
}
|
}
|
||||||
output += "\r\n"
|
output.WriteString("\r\n")
|
||||||
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedClients)))
|
output.WriteString(t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedClients))))
|
||||||
for _, traffic := range exhaustedClients {
|
for _, traffic := range exhaustedClients {
|
||||||
output += t.clientInfoMsg(&traffic, true, false, false, true, true, false)
|
output.WriteString(t.clientInfoMsg(&traffic, true, false, false, true, true, false))
|
||||||
output += "\r\n"
|
output.WriteString("\r\n")
|
||||||
}
|
}
|
||||||
t.SendMsgToTgbot(chatID, output)
|
t.SendMsgToTgbot(chatID, output.String())
|
||||||
}
|
}
|
||||||
chatIDsDone = append(chatIDsDone, chatID)
|
chatIDsDone = append(chatIDsDone, chatID)
|
||||||
}
|
}
|
||||||
|
|
@ -3480,12 +3479,7 @@ func (t *Tgbot) notifyExhausted() {
|
||||||
|
|
||||||
// int64Contains checks if an int64 slice contains a specific item.
|
// int64Contains checks if an int64 slice contains a specific item.
|
||||||
func int64Contains(slice []int64, item int64) bool {
|
func int64Contains(slice []int64, item int64) bool {
|
||||||
for _, s := range slice {
|
return slices.Contains(slice, item)
|
||||||
if s == item {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// onlineClients retrieves and sends information about online clients.
|
// onlineClients retrieves and sends information about online clients.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoginAttemptDoesNotCarryPassword(t *testing.T) {
|
func TestLoginAttemptDoesNotCarryPassword(t *testing.T) {
|
||||||
typ := reflect.TypeOf(LoginAttempt{})
|
typ := reflect.TypeFor[LoginAttempt]()
|
||||||
if _, ok := typ.FieldByName("Password"); ok {
|
if _, ok := typ.FieldByName("Password"); ok {
|
||||||
t.Fatal("LoginAttempt must not carry attempted passwords")
|
t.Fatal("LoginAttempt must not carry attempted passwords")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package service
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v3/util/common"
|
"github.com/mhsanaei/3x-ui/v3/util/common"
|
||||||
"github.com/mhsanaei/3x-ui/v3/xray"
|
"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.
|
// If `raw` does not look like a wrapper, it is returned unchanged.
|
||||||
func UnwrapXrayTemplateConfig(raw string) string {
|
func UnwrapXrayTemplateConfig(raw string) string {
|
||||||
const maxDepth = 8 // defensive cap against pathological multi-nest values
|
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
|
var top map[string]json.RawMessage
|
||||||
if err := json.Unmarshal([]byte(raw), &top); err != nil {
|
if err := json.Unmarshal([]byte(raw), &top); err != nil {
|
||||||
return raw
|
return raw
|
||||||
|
|
@ -190,10 +191,8 @@ func findApiRule(rules []map[string]any) int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case []string:
|
case []string:
|
||||||
for _, s := range tags {
|
if slices.Contains(tags, "api") {
|
||||||
if s == "api" {
|
return i
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case string:
|
case string:
|
||||||
if tags == "api" {
|
if tags == "api" {
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ func TestUnwrapXrayTemplateConfig(t *testing.T) {
|
||||||
// non-wrapped, and confirm we end up at some valid JSON (we
|
// non-wrapped, and confirm we end up at some valid JSON (we
|
||||||
// don't loop forever and we don't blow the stack).
|
// don't loop forever and we don't blow the stack).
|
||||||
s := real
|
s := real
|
||||||
for i := 0; i < 16; i++ {
|
for range 16 {
|
||||||
s = `{"xraySetting":` + s + `}`
|
s = `{"xraySetting":` + s + `}`
|
||||||
}
|
}
|
||||||
got := UnwrapXrayTemplateConfig(s)
|
got := UnwrapXrayTemplateConfig(s)
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ func (h *Hub) shouldThrottle(msgType MessageType) bool {
|
||||||
// panic doesn't permanently kill real-time updates for commercial deployments.
|
// 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.
|
// After the cap, the hub stays down and the frontend falls back to REST polling.
|
||||||
func (h *Hub) Run() {
|
func (h *Hub) Run() {
|
||||||
for attempt := 0; attempt < hubRestartAttempts; attempt++ {
|
for attempt := range hubRestartAttempts {
|
||||||
stopped := h.runOnce()
|
stopped := h.runOnce()
|
||||||
if stopped {
|
if stopped {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@ func TestHub_ConcurrentRegisterUnregister(t *testing.T) {
|
||||||
|
|
||||||
const n = 50
|
const n = 50
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
for i := 0; i < n; i++ {
|
for i := range n {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(idx int) {
|
go func(idx int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue