fix(clients): set a client's Telegram user on every inbound it is on

SetClientTelegramUserID patched tgId in the settings of only the
inbound the client's traffic row points at. Sibling inbounds kept the
old value, and the next sync of any of them wrote it back into the
client record, so removing a Telegram user from a multi-inbound client
silently re-bound that user to it.

The write now goes through applyClientFieldByEmail, as the other
by-email setters do, so every attached inbound, on the master or a
node, receives the new tgId.
This commit is contained in:
MHSanaei
2026-11-01 00:51:31 +01:00
parent 6a0c983c89
commit 2b9adaa6ff
2 changed files with 78 additions and 42 deletions
@@ -144,3 +144,76 @@ func TestSetClientEnableByEmail_MultiInbound(t *testing.T) {
} }
} }
} }
// Regression test: removing a client's Telegram user cleared tgId on one inbound
// only, so the next sync of the sibling bound the removed account again.
func TestSetClientTelegramUserID_MultiInbound(t *testing.T) {
dbDir := t.TempDir()
t.Setenv("XUI_DB_FOLDER", dbDir)
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
db := database.GetDB()
const email = "multitg@example.com"
const uid = "ce8d33df-3a64-4f10-8f9b-91c3a8e0c333"
clientJSON := `{"clients":[{"email":"` + email + `","id":"` + uid + `","enable":true,"subId":"sub-tg-1","tgId":4242}]}`
first := &model.Inbound{
Tag: "vless-tg-a", Enable: true, Port: 50021, Protocol: model.VLESS,
StreamSettings: `{"network":"tcp","security":"reality"}`, Settings: clientJSON,
}
second := &model.Inbound{
Tag: "vless-tg-b", Enable: true, Port: 50022, Protocol: model.VLESS,
StreamSettings: `{"network":"ws","security":"tls"}`, Settings: clientJSON,
}
for _, ib := range []*model.Inbound{first, second} {
if err := db.Create(ib).Error; err != nil {
t.Fatalf("create inbound %s: %v", ib.Tag, err)
}
}
clientSvc := ClientService{}
inboundSvc := InboundService{}
for _, ib := range []*model.Inbound{first, second} {
clients, err := inboundSvc.GetClients(ib)
if err != nil {
t.Fatalf("GetClients(%s): %v", ib.Tag, err)
}
if err := clientSvc.SyncInbound(nil, ib.Id, clients); err != nil {
t.Fatalf("SyncInbound(%s): %v", ib.Tag, err)
}
}
traffic := &xray.ClientTraffic{InboundId: first.Id, Email: email, Enable: true}
if err := db.Create(traffic).Error; err != nil {
t.Fatalf("seed traffic: %v", err)
}
if _, err := clientSvc.SetClientTelegramUserID(&inboundSvc, traffic.Id, 0); err != nil {
t.Fatalf("SetClientTelegramUserID: %v", err)
}
for _, ib := range []*model.Inbound{first, second} {
fresh, err := inboundSvc.GetInbound(ib.Id)
if err != nil {
t.Fatalf("GetInbound(%s): %v", ib.Tag, err)
}
clients, err := inboundSvc.GetClients(fresh)
if err != nil {
t.Fatalf("GetClients(%s): %v", ib.Tag, err)
}
if len(clients) != 1 || clients[0].TgID != 0 {
t.Errorf("inbound %s still binds the removed Telegram user: %+v", ib.Tag, clients)
}
if err := clientSvc.SyncInbound(nil, ib.Id, clients); err != nil {
t.Fatalf("SyncInbound(%s): %v", ib.Tag, err)
}
}
rec, err := clientSvc.GetRecordByEmail(nil, email)
if err != nil {
t.Fatalf("GetRecordByEmail: %v", err)
}
if rec.TgID != 0 {
t.Errorf("client record tgId = %d after the siblings were synced, want 0", rec.TgID)
}
}
+5 -42
View File
@@ -1339,48 +1339,11 @@ func (s *ClientService) SetClientTelegramUserID(inboundSvc *InboundService, traf
return false, common.NewError("Inbound Not Found For Traffic ID:", trafficId) return false, common.NewError("Inbound Not Found For Traffic ID:", trafficId)
} }
clientEmail := traffic.Email // On every attached inbound: a sibling left holding the old tgId hands it
// back to the client record on its next SyncInbound (#5039).
oldClients, err := inboundSvc.GetClients(inbound) return s.applyClientFieldByEmail(inboundSvc, traffic.Email, func(c map[string]any) {
if err != nil { c["tgId"] = tgId
return false, err })
}
found := false
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
found = true
break
}
}
if !found {
return false, common.NewError("Client Not Found For Email:", clientEmail)
}
var settings map[string]any
err = json.Unmarshal([]byte(inbound.Settings), &settings)
if err != nil {
return false, err
}
clients := settings["clients"].([]any)
var newClients []any
for client_index := range clients {
c := clients[client_index].(map[string]any)
if c["email"] == clientEmail {
c["tgId"] = tgId
c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c))
}
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return false, err
}
inbound.Settings = string(modifiedSettings)
needRestart, err := s.UpdateInboundClient(inboundSvc, inbound, clientEmail)
return needRestart, err
} }
func (s *ClientService) CheckIsEnabledByEmail(inboundSvc *InboundService, clientEmail string) (bool, error) { func (s *ClientService) CheckIsEnabledByEmail(inboundSvc *InboundService, clientEmail string) (bool, error) {