TGbot: improved (5x faster)
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run

This commit is contained in:
mhsanaei 2025-09-21 19:27:05 +02:00
parent b45e63a14a
commit 83f8a03b50
No known key found for this signature in database
GPG key ID: D875CD086CF668A0

View file

@ -16,6 +16,7 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/mhsanaei/3x-ui/v2/config" "github.com/mhsanaei/3x-ui/v2/config"
@ -44,6 +45,23 @@ var (
hostname string hostname string
hashStorage *global.HashStorage hashStorage *global.HashStorage
// Performance improvements
messageWorkerPool chan struct{} // Semaphore for limiting concurrent message processing
optimizedHTTPClient *http.Client // HTTP client with connection pooling and timeouts
// Simple cache for frequently accessed data
statusCache struct {
data *Status
timestamp time.Time
mutex sync.RWMutex
}
serverStatsCache struct {
data string
timestamp time.Time
mutex sync.RWMutex
}
// clients data to adding new client // clients data to adding new client
receiver_inbound_ID int receiver_inbound_ID int
client_Id string client_Id string
@ -100,6 +118,46 @@ func (t *Tgbot) GetHashStorage() *global.HashStorage {
return hashStorage return hashStorage
} }
// getCachedStatus returns cached server status if it's fresh enough (less than 5 seconds old)
func (t *Tgbot) getCachedStatus() (*Status, bool) {
statusCache.mutex.RLock()
defer statusCache.mutex.RUnlock()
if statusCache.data != nil && time.Since(statusCache.timestamp) < 5*time.Second {
return statusCache.data, true
}
return nil, false
}
// setCachedStatus updates the status cache
func (t *Tgbot) setCachedStatus(status *Status) {
statusCache.mutex.Lock()
defer statusCache.mutex.Unlock()
statusCache.data = status
statusCache.timestamp = time.Now()
}
// getCachedServerStats returns cached server stats if it's fresh enough (less than 10 seconds old)
func (t *Tgbot) getCachedServerStats() (string, bool) {
serverStatsCache.mutex.RLock()
defer serverStatsCache.mutex.RUnlock()
if serverStatsCache.data != "" && time.Since(serverStatsCache.timestamp) < 10*time.Second {
return serverStatsCache.data, true
}
return "", false
}
// setCachedServerStats updates the server stats cache
func (t *Tgbot) setCachedServerStats(stats string) {
serverStatsCache.mutex.Lock()
defer serverStatsCache.mutex.Unlock()
serverStatsCache.data = stats
serverStatsCache.timestamp = time.Now()
}
// Start initializes and starts the Telegram bot with the provided translation files. // Start initializes and starts the Telegram bot with the provided translation files.
func (t *Tgbot) Start(i18nFS embed.FS) error { func (t *Tgbot) Start(i18nFS embed.FS) error {
// Initialize localizer // Initialize localizer
@ -111,6 +169,20 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
// Initialize hash storage to store callback queries // Initialize hash storage to store callback queries
hashStorage = global.NewHashStorage(20 * time.Minute) hashStorage = global.NewHashStorage(20 * time.Minute)
// Initialize worker pool for concurrent message processing (max 10 concurrent handlers)
messageWorkerPool = make(chan struct{}, 10)
// Initialize optimized HTTP client with connection pooling
optimizedHTTPClient = &http.Client{
Timeout: 15 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
DisableKeepAlives: false,
},
}
t.SetHostname() t.SetHostname()
// Get Telegram bot token // Get Telegram bot token
@ -271,7 +343,7 @@ func (t *Tgbot) decodeQuery(query string) (string, error) {
// OnReceive starts the message receiving loop for the Telegram bot. // OnReceive starts the message receiving loop for the Telegram bot.
func (t *Tgbot) OnReceive() { func (t *Tgbot) OnReceive() {
params := telego.GetUpdatesParams{ params := telego.GetUpdatesParams{
Timeout: 10, Timeout: 30, // Increased timeout to reduce API calls
} }
updates, _ := bot.UpdatesViaLongPolling(context.Background(), &params) updates, _ := bot.UpdatesViaLongPolling(context.Background(), &params)
@ -285,14 +357,26 @@ func (t *Tgbot) OnReceive() {
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard"))) }, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
delete(userStates, message.Chat.ID) // Use goroutine with worker pool for concurrent command processing
t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID)) go func() {
messageWorkerPool <- struct{}{} // Acquire worker
defer func() { <-messageWorkerPool }() // Release worker
delete(userStates, message.Chat.ID)
t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
}()
return nil return nil
}, th.AnyCommand()) }, th.AnyCommand())
botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error { botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
delete(userStates, query.Message.GetChat().ID) // Use goroutine with worker pool for concurrent callback processing
t.answerCallback(&query, checkAdmin(query.From.ID)) go func() {
messageWorkerPool <- struct{}{} // Acquire worker
defer func() { <-messageWorkerPool }() // Release worker
delete(userStates, query.Message.GetChat().ID)
t.answerCallback(&query, checkAdmin(query.From.ID))
}()
return nil return nil
}, th.AnyCallbackQueryWithMessage()) }, th.AnyCallbackQueryWithMessage())
@ -2099,7 +2183,10 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
if err != nil { if err != nil {
logger.Warning("Error sending telegram message :", err) logger.Warning("Error sending telegram message :", err)
} }
time.Sleep(500 * time.Millisecond) // Reduced delay to improve performance (only needed for rate limiting)
if n < len(allMessages)-1 { // Only delay between messages, not after the last one
time.Sleep(100 * time.Millisecond)
}
} }
} }
@ -2208,12 +2295,12 @@ func (t *Tgbot) sendClientIndividualLinks(chatId int64, email string) {
// Force plain text to avoid HTML page; controller respects Accept header // Force plain text to avoid HTML page; controller respects Accept header
req.Header.Set("Accept", "text/plain, */*;q=0.1") req.Header.Set("Accept", "text/plain, */*;q=0.1")
// Use default client with reasonable timeout via context // Use optimized client with connection pooling
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
req = req.WithContext(ctx) req = req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req) resp, err := optimizedHTTPClient.Do(req)
if err != nil { if err != nil {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error()) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
return return
@ -2323,7 +2410,7 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
req = req.WithContext(ctx) req = req.WithContext(ctx)
if resp, err := http.DefaultClient.Do(req); err == nil { if resp, err := optimizedHTTPClient.Do(req); err == nil {
body, _ := io.ReadAll(resp.Body) body, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close() _ = resp.Body.Close()
encoded, _ := t.settingService.GetSubEncrypt() encoded, _ := t.settingService.GetSubEncrypt()
@ -2356,7 +2443,10 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
tu.FileFromBytes(png, filename), tu.FileFromBytes(png, filename),
) )
_, _ = bot.SendDocument(context.Background(), document) _, _ = bot.SendDocument(context.Background(), document)
time.Sleep(200 * time.Millisecond) // Reduced delay for better performance
if i < max-1 { // Only delay between documents, not after the last one
time.Sleep(50 * time.Millisecond)
}
} }
} }
} }
@ -2443,10 +2533,20 @@ func (t *Tgbot) sendServerUsage() string {
// prepareServerUsageInfo prepares the server usage information string. // prepareServerUsageInfo prepares the server usage information string.
func (t *Tgbot) prepareServerUsageInfo() string { func (t *Tgbot) prepareServerUsageInfo() string {
// Check if we have cached data first
if cachedStats, found := t.getCachedServerStats(); found {
return cachedStats
}
info, ipv4, ipv6 := "", "", "" info, ipv4, ipv6 := "", "", ""
// get latest status of server // get latest status of server with caching
t.lastStatus = t.serverService.GetStatus(t.lastStatus) if cachedStatus, found := t.getCachedStatus(); found {
t.lastStatus = cachedStatus
} else {
t.lastStatus = t.serverService.GetStatus(t.lastStatus)
t.setCachedStatus(t.lastStatus)
}
onlines := p.GetOnlineClients() onlines := p.GetOnlineClients()
info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
@ -2488,6 +2588,10 @@ func (t *Tgbot) prepareServerUsageInfo() string {
info += t.I18nBot("tgbot.messages.udpCount", "Count=="+strconv.Itoa(t.lastStatus.UdpCount)) info += t.I18nBot("tgbot.messages.udpCount", "Count=="+strconv.Itoa(t.lastStatus.UdpCount))
info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), "Upload=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), "Download=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv))) info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), "Upload=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), "Download=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv)))
info += t.I18nBot("tgbot.messages.xrayStatus", "State=="+fmt.Sprint(t.lastStatus.Xray.State)) info += t.I18nBot("tgbot.messages.xrayStatus", "State=="+fmt.Sprint(t.lastStatus.Xray.State))
// Cache the complete server stats
t.setCachedServerStats(info)
return info return info
} }