mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
Fix Telegram bot client lookup for multi-node inbounds
This commit is contained in:
parent
a07b68894c
commit
c64cdbb0ed
2 changed files with 251 additions and 32 deletions
|
|
@ -2620,10 +2620,144 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
|
|||
|
||||
func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffic, error) {
|
||||
db := database.GetDB()
|
||||
|
||||
var emails []string
|
||||
|
||||
// Prefer the normalized clients table. It is populated for both local
|
||||
// inbounds and remote-node snapshots, and avoids brittle JSON LIKE matching
|
||||
// against settings formatting.
|
||||
err := db.Table("clients").
|
||||
Select("DISTINCT clients.email").
|
||||
Joins("JOIN client_inbounds ON client_inbounds.client_id = clients.id").
|
||||
Joins("JOIN inbounds ON inbounds.id = client_inbounds.inbound_id").
|
||||
Where("clients.tg_id = ? AND clients.email <> ''", tgId).
|
||||
Pluck("clients.email", &emails).Error
|
||||
if err != nil {
|
||||
logger.Errorf("Error retrieving client emails for tgId %d: %v", tgId, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(emails) == 0 {
|
||||
emails, err = s.getClientTrafficTgBotFromSettings(tgId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
uniqEmails := uniqueNonEmptyStrings(emails)
|
||||
if len(uniqEmails) == 0 {
|
||||
logger.Warningf("No clients found for tgId: %d", tgId)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
trafficsByEmail := make(map[string]*xray.ClientTraffic, 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 == gorm.ErrRecordNotFound {
|
||||
continue
|
||||
}
|
||||
logger.Errorf("Error retrieving ClientTraffic for emails %v: %v", batch, err)
|
||||
return nil, err
|
||||
}
|
||||
for _, traffic := range page {
|
||||
trafficsByEmail[traffic.Email] = traffic
|
||||
}
|
||||
}
|
||||
|
||||
missingEmails := make([]string, 0)
|
||||
for _, email := range uniqEmails {
|
||||
if _, ok := trafficsByEmail[email]; !ok {
|
||||
missingEmails = append(missingEmails, email)
|
||||
}
|
||||
}
|
||||
if len(missingEmails) > 0 {
|
||||
type clientTrafficSeed struct {
|
||||
Email string
|
||||
InboundId int
|
||||
Enable bool
|
||||
TotalGB int64
|
||||
ExpiryTime int64
|
||||
Reset int
|
||||
UUID string
|
||||
SubID string
|
||||
}
|
||||
var rows []clientTrafficSeed
|
||||
for _, batch := range chunkStrings(missingEmails, sqliteMaxVars) {
|
||||
var page []clientTrafficSeed
|
||||
if err = db.Table("clients").
|
||||
Select(`clients.email,
|
||||
client_inbounds.inbound_id,
|
||||
clients.enable,
|
||||
clients.total_gb,
|
||||
clients.expiry_time,
|
||||
clients.reset,
|
||||
clients.uuid,
|
||||
clients.sub_id`).
|
||||
Joins("JOIN client_inbounds ON client_inbounds.client_id = clients.id").
|
||||
Where("clients.email IN ?", batch).
|
||||
Find(&page).Error; err != nil {
|
||||
logger.Errorf("Error retrieving client records for emails %v: %v", batch, err)
|
||||
return nil, err
|
||||
}
|
||||
rows = append(rows, page...)
|
||||
}
|
||||
for _, row := range rows {
|
||||
if _, ok := trafficsByEmail[row.Email]; ok {
|
||||
continue
|
||||
}
|
||||
trafficsByEmail[row.Email] = &xray.ClientTraffic{
|
||||
InboundId: row.InboundId,
|
||||
Email: row.Email,
|
||||
Enable: row.Enable,
|
||||
Total: row.TotalGB,
|
||||
ExpiryTime: row.ExpiryTime,
|
||||
Reset: row.Reset,
|
||||
UUID: row.UUID,
|
||||
SubId: row.SubID,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traffics := make([]*xray.ClientTraffic, 0, len(uniqEmails))
|
||||
for _, email := range uniqEmails {
|
||||
traffic, ok := trafficsByEmail[email]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ct, client, e := s.GetClientByEmail(email); e == nil && ct != nil && client != nil {
|
||||
traffic.Enable = client.Enable
|
||||
traffic.UUID = client.ID
|
||||
traffic.SubId = client.SubID
|
||||
} else if traffic.UUID == "" || traffic.SubId == "" {
|
||||
clients, clientErr := s.clientService.ListForInbound(nil, traffic.InboundId)
|
||||
if clientErr != nil {
|
||||
logger.Errorf("Error retrieving clients for inbound %d: %v", traffic.InboundId, clientErr)
|
||||
}
|
||||
for _, client := range clients {
|
||||
if client.Email == email {
|
||||
traffic.Enable = client.Enable
|
||||
traffic.UUID = client.ID
|
||||
traffic.SubId = client.SubID
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
traffics = append(traffics, traffic)
|
||||
}
|
||||
if len(traffics) == 0 {
|
||||
logger.Warning("No ClientTraffic records found for emails:", emails)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return traffics, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) getClientTrafficTgBotFromSettings(tgId int64) ([]string, 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
|
||||
err := db.Model(model.Inbound{}).Where("settings LIKE ?", fmt.Sprintf(`%%"tgId"%%%d%%`, tgId)).Find(&inbounds).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
logger.Errorf("Error retrieving inbounds with tgId %d: %v", tgId, err)
|
||||
return nil, err
|
||||
|
|
@ -2643,36 +2777,7 @@ func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffi
|
|||
}
|
||||
}
|
||||
|
||||
// Chunked to stay under SQLite's bind-variable limit when a single Telegram
|
||||
// account owns thousands of clients across inbounds.
|
||||
uniqEmails := uniqueNonEmptyStrings(emails)
|
||||
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 == gorm.ErrRecordNotFound {
|
||||
continue
|
||||
}
|
||||
logger.Errorf("Error retrieving ClientTraffic for emails %v: %v", batch, err)
|
||||
return nil, err
|
||||
}
|
||||
traffics = append(traffics, page...)
|
||||
}
|
||||
if len(traffics) == 0 {
|
||||
logger.Warning("No ClientTraffic records found for emails:", emails)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Populate UUID and other client data for each traffic record
|
||||
for i := range traffics {
|
||||
if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil {
|
||||
traffics[i].Enable = client.Enable
|
||||
traffics[i].UUID = client.ID
|
||||
traffics[i].SubId = client.SubID
|
||||
}
|
||||
}
|
||||
|
||||
return traffics, nil
|
||||
return emails, nil
|
||||
}
|
||||
|
||||
// sqliteMaxVars is a safe ceiling for the number of bind parameters in a
|
||||
|
|
|
|||
114
web/service/tgbot_client_traffic_test.go
Normal file
114
web/service/tgbot_client_traffic_test.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/xray"
|
||||
)
|
||||
|
||||
func setupTgBotTrafficTestDB(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
dbDir := t.TempDir()
|
||||
t.Setenv("XUI_DB_FOLDER", dbDir)
|
||||
if err := database.InitDB(filepath.Join(dbDir, "3x-ui.db")); err != nil {
|
||||
t.Fatalf("InitDB: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.CloseDB() })
|
||||
}
|
||||
|
||||
func TestGetClientTrafficTgBotUsesNormalizedRemoteNodeClients(t *testing.T) {
|
||||
setupTgBotTrafficTestDB(t)
|
||||
|
||||
db := database.GetDB()
|
||||
nodeID := 7
|
||||
inbound := &model.Inbound{
|
||||
NodeID: &nodeID,
|
||||
Tag: "node-vless",
|
||||
Enable: true,
|
||||
Port: 10001,
|
||||
Protocol: model.VLESS,
|
||||
Settings: `{"clients":[]}`,
|
||||
}
|
||||
if err := db.Create(inbound).Error; err != nil {
|
||||
t.Fatalf("create inbound: %v", err)
|
||||
}
|
||||
|
||||
const tgID int64 = 505739390
|
||||
const email = "remote-user@example.com"
|
||||
const uuid = "ce8d33df-3a64-4f10-8f9b-91c3a8e0c010"
|
||||
const subID = "remote-sub-id"
|
||||
|
||||
clientSvc := ClientService{}
|
||||
if err := clientSvc.SyncInbound(nil, inbound.Id, []model.Client{{
|
||||
Email: email,
|
||||
ID: uuid,
|
||||
SubID: subID,
|
||||
Enable: true,
|
||||
TgID: tgID,
|
||||
}}); err != nil {
|
||||
t.Fatalf("SyncInbound: %v", err)
|
||||
}
|
||||
if err := db.Create(&xray.ClientTraffic{
|
||||
InboundId: inbound.Id,
|
||||
Email: email,
|
||||
Enable: true,
|
||||
Total: 1024,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create traffic: %v", err)
|
||||
}
|
||||
|
||||
traffics, err := (&InboundService{}).GetClientTrafficTgBot(tgID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetClientTrafficTgBot: %v", err)
|
||||
}
|
||||
if len(traffics) != 1 {
|
||||
t.Fatalf("expected one traffic row, got %d", len(traffics))
|
||||
}
|
||||
if traffics[0].Email != email || traffics[0].UUID != uuid || traffics[0].SubId != subID {
|
||||
t.Fatalf("unexpected traffic: %#v", traffics[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClientTrafficTgBotFallsBackToCompactSettingsJSON(t *testing.T) {
|
||||
setupTgBotTrafficTestDB(t)
|
||||
|
||||
db := database.GetDB()
|
||||
const tgID int64 = 505739390
|
||||
const email = "legacy-user@example.com"
|
||||
const uuid = "ce8d33df-3a64-4f10-8f9b-91c3a8e0c011"
|
||||
const subID = "legacy-sub-id"
|
||||
|
||||
inbound := &model.Inbound{
|
||||
Tag: "legacy-vless",
|
||||
Enable: true,
|
||||
Port: 10002,
|
||||
Protocol: model.VLESS,
|
||||
Settings: fmt.Sprintf(`{"clients":[{"email":%q,"id":%q,"subId":%q,"enable":true,"tgId":%d}]}`, email, uuid, subID, tgID),
|
||||
}
|
||||
if err := db.Create(inbound).Error; err != nil {
|
||||
t.Fatalf("create inbound: %v", err)
|
||||
}
|
||||
if err := db.Create(&xray.ClientTraffic{
|
||||
InboundId: inbound.Id,
|
||||
Email: email,
|
||||
Enable: true,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create traffic: %v", err)
|
||||
}
|
||||
|
||||
traffics, err := (&InboundService{}).GetClientTrafficTgBot(tgID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetClientTrafficTgBot: %v", err)
|
||||
}
|
||||
if len(traffics) != 1 {
|
||||
t.Fatalf("expected one traffic row, got %d", len(traffics))
|
||||
}
|
||||
if traffics[0].Email != email || traffics[0].UUID != uuid || traffics[0].SubId != subID {
|
||||
t.Fatalf("unexpected traffic: %#v", traffics[0])
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue