fix(tgbot): look up a Telegram account's clients through the indexed client table (#6887)

Every bot usage command and the hourly report resolved a Telegram ID by
expanding every inbound's settings.clients, re-parsing the matched inbounds and
looking up each client again by email. With thousands of clients this ran
thousands of times per report and showed up as high PostgreSQL CPU, and an
account whose lookup came back empty was retried once per client carrying it.

The lookup now uses the normalized clients table and its Telegram-ID index in
one query per account, and the hourly pass checks each account once. Bot output
and thresholds are unchanged; link callbacks now authorize against the current
client records instead of possibly stale settings JSON.
This commit is contained in:
Hamed
2026-10-24 07:25:19 +03:30
committed by GitHub
parent d2a0216e01
commit 7d45456feb
9 changed files with 378 additions and 85 deletions
+27 -63
View File
@@ -7,7 +7,6 @@ import (
"fmt"
"maps"
"slices"
"strconv"
"strings"
"time"
@@ -1057,72 +1056,37 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
return nil
}
// ClientsTable migrates legacy JSON at startup; normal writes synchronize these
// records. Never fall back to settings JSON, which can retain stale ownership.
func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffic, error) {
db := database.GetDB()
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)
if err != nil {
logger.Errorf("Error retrieving clients for inbound %d: %v", inbound.Id, err)
continue
}
for _, client := range clients {
if client.TgID == tgId {
emails = append(emails, client.Email)
}
}
}
// 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 errors.Is(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)
if tgId <= 0 {
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
}
type row struct {
xray.ClientTraffic
ClientEnable bool
ClientUUID string
ClientSubID string
}
var rows []row
err := database.GetDB().Table("clients").
Select("client_traffics.*, clients.enable AS client_enable, clients.uuid AS client_uuid, clients.sub_id AS client_sub_id").
Joins("JOIN client_traffics ON client_traffics.email = clients.email").
Where("clients.tg_id = ?", tgId).
Where("EXISTS (SELECT 1 FROM client_inbounds JOIN inbounds ON inbounds.id = client_inbounds.inbound_id WHERE client_inbounds.client_id = clients.id)").
Order("clients.id ASC").
Find(&rows).Error
if err != nil {
return nil, err
}
traffics := make([]*xray.ClientTraffic, 0, len(rows))
for i := range rows {
traffic := &rows[i].ClientTraffic
traffic.Enable = rows[i].ClientEnable
traffic.UUID = rows[i].ClientUUID
traffic.SubId = rows[i].ClientSubID
traffics = append(traffics, traffic)
}
return traffics, nil
}