diff --git a/internal/web/service/inbound_tgbot_lookup_test.go b/internal/web/service/inbound_tgbot_lookup_test.go new file mode 100644 index 000000000..95cd056c2 --- /dev/null +++ b/internal/web/service/inbound_tgbot_lookup_test.go @@ -0,0 +1,67 @@ +package service + +import ( + "path/filepath" + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/model" + "github.com/mhsanaei/3x-ui/v3/internal/xray" +) + +// TestGetClientTrafficTgBot_SettingsSerializationStyles guards against the +// prefilter regressing into a formatting-sensitive string match (#5805): the +// lookup must find clients whether inbounds.settings stores compact JSON +// ("tgId":N, as written by node sync/import) or indented JSON ("tgId": N, as +// written by the panel's MarshalIndent). +func TestGetClientTrafficTgBot_SettingsSerializationStyles(t *testing.T) { + dbDir := t.TempDir() + t.Setenv("XUI_DB_FOLDER", dbDir) + if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { + t.Fatalf("InitDB: %v", err) + } + t.Cleanup(func() { _ = database.CloseDB() }) + + db := database.GetDB() + + const tgId int64 = 123456789 + cases := []struct { + name string + settings string + email string + port int + }{ + {"compact", `{"clients":[{"id":"u1","email":"compact-user","tgId":123456789}]}`, "compact-user", 41001}, + {"spaced", `{"clients": [{"id": "u2", "email": "spaced-user", "tgId": 123456789}]}`, "spaced-user", 41002}, + } + for _, c := range cases { + inbound := &model.Inbound{UserId: 1, Tag: "tg-" + c.name, Enable: true, Port: c.port, Protocol: model.VLESS, Settings: c.settings} + if err := db.Create(inbound).Error; err != nil { + t.Fatalf("create %s inbound: %v", c.name, err) + } + if err := db.Create(&xray.ClientTraffic{InboundId: inbound.Id, Email: c.email, Enable: true, Up: 10, Down: 20}).Error; err != nil { + t.Fatalf("create %s client_traffics: %v", c.name, err) + } + } + + svc := InboundService{} + traffics, err := svc.GetClientTrafficTgBot(tgId) + if err != nil { + t.Fatalf("GetClientTrafficTgBot: %v", err) + } + got := make(map[string]bool, len(traffics)) + for _, tr := range traffics { + got[tr.Email] = true + } + if len(traffics) != 2 || !got["compact-user"] || !got["spaced-user"] { + t.Fatalf("expected traffic for compact-user and spaced-user, got %v", got) + } + + other, err := svc.GetClientTrafficTgBot(42) + if err != nil { + t.Fatalf("GetClientTrafficTgBot(42): %v", err) + } + if len(other) != 0 { + t.Fatalf("expected no traffic for unknown tgId, got %d rows", len(other)) + } +} diff --git a/internal/web/service/inbound_traffic.go b/internal/web/service/inbound_traffic.go index 4ce82ce12..b5881d592 100644 --- a/internal/web/service/inbound_traffic.go +++ b/internal/web/service/inbound_traffic.go @@ -7,6 +7,7 @@ import ( "fmt" "maps" "slices" + "strconv" "strings" "time" @@ -837,15 +838,27 @@ func (s *InboundService) DelDepletedClients(id int) (err error) { func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffic, error) { db := database.GetDB() - var inbounds []*model.Inbound - // Retrieve inbounds where settings contain the given tgId - err := db.Model(model.Inbound{}).Where("settings LIKE ?", fmt.Sprintf(`%%"tgId": %d%%`, tgId)).Find(&inbounds).Error - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + idQuery := fmt.Sprintf( + "SELECT DISTINCT inbounds.id %s WHERE %s = ?", + database.JSONClientsFromInbound(), + database.JSONFieldText("client.value", "tgId"), + ) + var inboundIds []int + if err := db.Raw(idQuery, strconv.FormatInt(tgId, 10)).Scan(&inboundIds).Error; err != nil { logger.Errorf("Error retrieving inbounds with tgId %d: %v", tgId, err) return nil, err } + var inbounds []*model.Inbound + if len(inboundIds) > 0 { + err := db.Model(model.Inbound{}).Where("id IN ?", inboundIds).Find(&inbounds).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Errorf("Error retrieving inbounds with tgId %d: %v", tgId, err) + return nil, err + } + } + var emails []string for _, inbound := range inbounds { clients, err := s.GetClients(inbound) @@ -866,7 +879,7 @@ func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffi traffics := make([]*xray.ClientTraffic, 0, len(uniqEmails)) for _, batch := range chunkStrings(uniqEmails, sqliteMaxVars) { var page []*xray.ClientTraffic - if err = db.Model(xray.ClientTraffic{}).Where("email IN ?", batch).Find(&page).Error; err != nil { + if err := db.Model(xray.ClientTraffic{}).Where("email IN ?", batch).Find(&page).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { continue }