From b6183271daa82520942b9e575533193236c71ee1 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 5 Jul 2026 20:18:59 +0200 Subject: [PATCH] fix(tgbot): find clients by tgId regardless of settings JSON formatting The Telegram-bot usage lookup prefiltered inbounds with settings LIKE '%"tgId": N%', which requires the exact space the panel's MarshalIndent happens to emit. Inbounds whose settings were serialized compactly (node sync, imports, external edits) never matched, so the bot reported no configuration even though the client and traffic rows exist. Replace the string match with the driver-portable JSON helpers already used by GetAllEmails, which read the actual clients array on SQLite and Postgres alike. Closes #5805 --- .../web/service/inbound_tgbot_lookup_test.go | 67 +++++++++++++++++++ internal/web/service/inbound_traffic.go | 23 +++++-- 2 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 internal/web/service/inbound_tgbot_lookup_test.go 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 }