mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-14 09:05:01 +03:00
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:
@@ -2,6 +2,7 @@ package tgbot
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -10,13 +11,16 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mymmrac/telego"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
|
||||
"github.com/mymmrac/telego"
|
||||
)
|
||||
|
||||
// seedReportClients writes one inbound holding every email plus the traffic row
|
||||
@@ -185,3 +189,92 @@ func TestTrafficUsageReportEscapesHtml(t *testing.T) {
|
||||
t.Errorf("report text = %q, want the remark escaped as <fast>", got[0].Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifyExhaustedLooksUpEachIDOnce(t *testing.T) {
|
||||
for _, outcome := range []string{"empty", "failure", "exhausted", "expiry", "admin", "zero", "disabled-inbound"} {
|
||||
t.Run(outcome, func(t *testing.T) {
|
||||
tb := initReportDB(t)
|
||||
clientInfoLocalizer(t)
|
||||
mock, sent := captureReportServer(t)
|
||||
swapTestBot(t, mock.URL)
|
||||
t.Cleanup(mock.Close)
|
||||
clients := []model.Client{
|
||||
{Email: "notify-a", ID: "uuid-a", TgID: 6886, Enable: true},
|
||||
{Email: "notify-b", ID: "uuid-b", TgID: 6886, Enable: true},
|
||||
}
|
||||
if outcome == "zero" {
|
||||
for i := range clients {
|
||||
clients[i].TgID = 0
|
||||
}
|
||||
}
|
||||
if outcome == "admin" {
|
||||
tgBotMutex.Lock()
|
||||
previous := adminIds
|
||||
adminIds = append(append([]int64(nil), adminIds...), 6886)
|
||||
tgBotMutex.Unlock()
|
||||
t.Cleanup(func() {
|
||||
tgBotMutex.Lock()
|
||||
adminIds = previous
|
||||
tgBotMutex.Unlock()
|
||||
})
|
||||
}
|
||||
db := database.GetDB()
|
||||
for i := range 2 {
|
||||
inbound := &model.Inbound{UserId: 1, Tag: fmt.Sprintf("notify-%d", i), Port: 41890 + i, Enable: true, Protocol: model.VLESS, Settings: `{"clients":[{"email":"notify-a","id":"uuid-a","enable":true,"tgId":6886},{"email":"notify-b","id":"uuid-b","enable":true,"tgId":6886}]}`}
|
||||
if outcome == "disabled-inbound" {
|
||||
inbound.Enable = false
|
||||
}
|
||||
if err := db.Create(inbound).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := (&service.ClientService{}).SyncInbound(nil, inbound.Id, clients); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Keep the inbound eligible even when the Telegram account has no traffic.
|
||||
traffic := &xray.ClientTraffic{Email: fmt.Sprintf("unrelated-%d", i), InboundId: inbound.Id}
|
||||
if outcome == "exhausted" || outcome == "expiry" {
|
||||
traffic.Email = clients[i].Email
|
||||
traffic.Enable = true
|
||||
traffic.Total = 1
|
||||
traffic.Up = 2
|
||||
if outcome == "expiry" {
|
||||
traffic.Total = 0
|
||||
traffic.ExpiryTime = time.Now().Add(-time.Hour).UnixMilli()
|
||||
}
|
||||
}
|
||||
if err := db.Create(traffic).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
lookups := 0
|
||||
cb := "test:notify_lookup"
|
||||
if err := db.Callback().Query().Before("gorm:query").Register(cb, func(tx *gorm.DB) {
|
||||
if tx.Statement.Table == "clients" && strings.Contains(fmt.Sprint(tx.Statement.Clauses["WHERE"]), "tg_id") {
|
||||
lookups++
|
||||
if outcome == "failure" {
|
||||
tx.AddError(errors.New("injected Telegram lookup failure"))
|
||||
}
|
||||
}
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Callback().Query().Remove(cb) })
|
||||
tb.notifyExhausted()
|
||||
wantLookups := 1
|
||||
if outcome == "admin" || outcome == "zero" || outcome == "disabled-inbound" {
|
||||
wantLookups = 0
|
||||
}
|
||||
if lookups != wantLookups {
|
||||
t.Fatalf("lookups = %d, want %d after %s", lookups, wantLookups, outcome)
|
||||
}
|
||||
got := sent()
|
||||
if outcome == "exhausted" || outcome == "expiry" {
|
||||
if len(got) != 1 || !strings.Contains(got[0].Text, "notify-a") || !strings.Contains(got[0].Text, "notify-b") {
|
||||
t.Fatalf("want one notification containing both clients, got %+v", got)
|
||||
}
|
||||
} else if len(got) != 0 {
|
||||
t.Fatalf("unexpected notifications: %+v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user