mirror of
				https://github.com/MHSanaei/3x-ui.git
				synced 2025-10-26 10:04:41 +00:00 
			
		
		
		
	Compare commits
	
		
			4 commits
		
	
	
		
			83f8a03b50
			...
			1016f3b4f9
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 1016f3b4f9 | ||
|   | 020bc9d77c | ||
|   | 5620d739c6 | ||
|   | d518979e4f | 
					 9 changed files with 103 additions and 48 deletions
				
			
		|  | @ -1 +1 @@ | |||
| 2.8.2 | ||||
| 2.8.3 | ||||
							
								
								
									
										6
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								go.mod
									
									
									
									
									
								
							|  | @ -5,7 +5,7 @@ go 1.25.1 | |||
| require ( | ||||
| 	github.com/gin-contrib/gzip v1.2.3 | ||||
| 	github.com/gin-contrib/sessions v1.0.4 | ||||
| 	github.com/gin-gonic/gin v1.10.1 | ||||
| 	github.com/gin-gonic/gin v1.11.0 | ||||
| 	github.com/goccy/go-json v0.10.5 | ||||
| 	github.com/google/uuid v1.6.0 | ||||
| 	github.com/joho/godotenv v1.5.1 | ||||
|  | @ -36,13 +36,14 @@ require ( | |||
| 	github.com/cloudflare/circl v1.6.1 // indirect | ||||
| 	github.com/cloudwego/base64x v0.1.6 // indirect | ||||
| 	github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect | ||||
| 	github.com/ebitengine/purego v0.8.4 // indirect | ||||
| 	github.com/ebitengine/purego v0.9.0 // indirect | ||||
| 	github.com/gabriel-vasile/mimetype v1.4.10 // indirect | ||||
| 	github.com/gin-contrib/sse v1.1.0 // indirect | ||||
| 	github.com/go-ole/go-ole v1.3.0 // indirect | ||||
| 	github.com/go-playground/locales v0.14.1 // indirect | ||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||
| 	github.com/go-playground/validator/v10 v10.27.0 // indirect | ||||
| 	github.com/goccy/go-yaml v1.18.0 // indirect | ||||
| 	github.com/google/btree v1.1.3 // indirect | ||||
| 	github.com/gorilla/context v1.1.2 // indirect | ||||
| 	github.com/gorilla/securecookie v1.1.2 // indirect | ||||
|  | @ -96,7 +97,6 @@ require ( | |||
| 	golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect | ||||
| 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect | ||||
| 	google.golang.org/protobuf v1.36.9 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| 	gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect | ||||
| 	lukechampine.com/blake3 v1.4.1 // indirect | ||||
| ) | ||||
|  |  | |||
							
								
								
									
										10
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								go.sum
									
									
									
									
									
								
							|  | @ -19,8 +19,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs | |||
| github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= | ||||
| github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM= | ||||
| github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= | ||||
| github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= | ||||
| github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= | ||||
| github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k= | ||||
| github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= | ||||
| github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= | ||||
| github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= | ||||
| github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= | ||||
|  | @ -31,8 +31,8 @@ github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kb | |||
| github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs= | ||||
| github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= | ||||
| github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= | ||||
| github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= | ||||
| github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= | ||||
| github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= | ||||
| github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= | ||||
| github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= | ||||
| github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= | ||||
| github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= | ||||
|  | @ -50,6 +50,8 @@ github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHO | |||
| github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= | ||||
| github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= | ||||
| github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= | ||||
| github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= | ||||
| github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= | ||||
| github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= | ||||
| github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= | ||||
| github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= | ||||
|  |  | |||
|  | @ -1077,20 +1077,73 @@ func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, | |||
| 	return | ||||
| } | ||||
| 
 | ||||
| // BuildURLs constructs absolute subscription and json URLs.
 | ||||
| // BuildURLs constructs subscription and JSON subscription URLs for a given subscription ID.
 | ||||
| // BuildURLs constructs absolute subscription and JSON subscription URLs for a given subscription ID.
 | ||||
| // It prioritizes configured URIs, then individual settings, and finally falls back to request-derived components.
 | ||||
| func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) { | ||||
| 	if strings.HasSuffix(subPath, "/") { | ||||
| 		subURL = scheme + "://" + hostWithPort + subPath + subId | ||||
| 	} else { | ||||
| 		subURL = scheme + "://" + hostWithPort + strings.TrimRight(subPath, "/") + "/" + subId | ||||
| 	// Input validation
 | ||||
| 	if subId == "" { | ||||
| 		return "", "" | ||||
| 	} | ||||
| 	if strings.HasSuffix(subJsonPath, "/") { | ||||
| 		subJsonURL = scheme + "://" + hostWithPort + subJsonPath + subId | ||||
| 	} else { | ||||
| 		subJsonURL = scheme + "://" + hostWithPort + strings.TrimRight(subJsonPath, "/") + "/" + subId | ||||
| 
 | ||||
| 	// Get configured URIs first (highest priority)
 | ||||
| 	configuredSubURI, _ := s.settingService.GetSubURI() | ||||
| 	configuredSubJsonURI, _ := s.settingService.GetSubJsonURI() | ||||
| 
 | ||||
| 	// Determine base scheme and host (cached to avoid duplicate calls)
 | ||||
| 	var baseScheme, baseHostWithPort string | ||||
| 	if configuredSubURI == "" || configuredSubJsonURI == "" { | ||||
| 		baseScheme, baseHostWithPort = s.getBaseSchemeAndHost(scheme, hostWithPort) | ||||
| 	} | ||||
| 	return | ||||
| 
 | ||||
| 	// Build subscription URL
 | ||||
| 	subURL = s.buildSingleURL(configuredSubURI, baseScheme, baseHostWithPort, subPath, subId) | ||||
| 
 | ||||
| 	// Build JSON subscription URL
 | ||||
| 	subJsonURL = s.buildSingleURL(configuredSubJsonURI, baseScheme, baseHostWithPort, subJsonPath, subId) | ||||
| 
 | ||||
| 	return subURL, subJsonURL | ||||
| } | ||||
| 
 | ||||
| // getBaseSchemeAndHost determines the base scheme and host from settings or falls back to request values
 | ||||
| func (s *SubService) getBaseSchemeAndHost(requestScheme, requestHostWithPort string) (string, string) { | ||||
| 	subDomain, err := s.settingService.GetSubDomain() | ||||
| 	if err != nil || subDomain == "" { | ||||
| 		return requestScheme, requestHostWithPort | ||||
| 	} | ||||
| 
 | ||||
| 	// Get port and TLS settings
 | ||||
| 	subPort, _ := s.settingService.GetSubPort() | ||||
| 	subKeyFile, _ := s.settingService.GetSubKeyFile() | ||||
| 	subCertFile, _ := s.settingService.GetSubCertFile() | ||||
| 
 | ||||
| 	// Determine scheme from TLS configuration
 | ||||
| 	scheme := "http" | ||||
| 	if subKeyFile != "" && subCertFile != "" { | ||||
| 		scheme = "https" | ||||
| 	} | ||||
| 
 | ||||
| 	// Build host:port, always include port for clarity
 | ||||
| 	hostWithPort := fmt.Sprintf("%s:%d", subDomain, subPort) | ||||
| 
 | ||||
| 	return scheme, hostWithPort | ||||
| } | ||||
| 
 | ||||
| // buildSingleURL constructs a single URL using configured URI or base components
 | ||||
| func (s *SubService) buildSingleURL(configuredURI, baseScheme, baseHostWithPort, basePath, subId string) string { | ||||
| 	if configuredURI != "" { | ||||
| 		return s.joinPathWithID(configuredURI, subId) | ||||
| 	} | ||||
| 
 | ||||
| 	baseURL := fmt.Sprintf("%s://%s", baseScheme, baseHostWithPort) | ||||
| 	return s.joinPathWithID(baseURL+basePath, subId) | ||||
| } | ||||
| 
 | ||||
| // joinPathWithID safely joins a base path with a subscription ID
 | ||||
| func (s *SubService) joinPathWithID(basePath, subId string) string { | ||||
| 	if strings.HasSuffix(basePath, "/") { | ||||
| 		return basePath + subId | ||||
| 	} | ||||
| 	return basePath + "/" + subId | ||||
| } | ||||
| 
 | ||||
| // BuildPageData parses header and prepares the template view model.
 | ||||
|  | @ -1103,10 +1156,7 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray | |||
| 	remained := "" | ||||
| 	if traffic.Total > 0 { | ||||
| 		total = common.FormatTraffic(traffic.Total) | ||||
| 		left := traffic.Total - (traffic.Up + traffic.Down) | ||||
| 		if left < 0 { | ||||
| 			left = 0 | ||||
| 		} | ||||
| 		left := max(traffic.Total-(traffic.Up+traffic.Down), 0) | ||||
| 		remained = common.FormatTraffic(left) | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ class AllSetting { | |||
|         this.webKeyFile = ""; | ||||
|         this.webBasePath = "/"; | ||||
|         this.sessionMaxAge = 360; | ||||
|         this.pageSize = 50; | ||||
|         this.pageSize = 25; | ||||
|         this.expireDiff = 0; | ||||
|         this.trafficDiff = 0; | ||||
|         this.remarkModel = "-ieo"; | ||||
|  |  | |||
|  | @ -736,7 +736,7 @@ | |||
|       refreshing: false, | ||||
|       refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000, | ||||
|       subSettings: { | ||||
|         enable: true, | ||||
|         enable: false, | ||||
|         subTitle: '', | ||||
|         subURI: '', | ||||
|         subJsonURI: '', | ||||
|  | @ -747,7 +747,7 @@ | |||
|       tgBotEnable: false, | ||||
|       showAlert: false, | ||||
|       ipLimitEnable: false, | ||||
|       pageSize: 50, | ||||
|       pageSize: 0, | ||||
|     }, | ||||
|     methods: { | ||||
|       loading(spinning = true) { | ||||
|  |  | |||
|  | @ -12,13 +12,14 @@ | |||
|     <a-layout-content> | ||||
|       <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'> | ||||
|         <transition name="list" appear> | ||||
|           <a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }" message='{{ i18n "secAlertTitle" }}' | ||||
|             color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable> | ||||
|           <a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }" | ||||
|             message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable> | ||||
|           </a-alert> | ||||
|         </transition> | ||||
|         <transition name="list" appear> | ||||
|           <a-row v-if="!loadingStates.fetched"> | ||||
|             <a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }"> | ||||
|             <a-card | ||||
|               :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }"> | ||||
|               <a-spin tip='{{ i18n "loading" }}'></a-spin> | ||||
|             </a-card> | ||||
|           </a-row> | ||||
|  | @ -37,7 +38,8 @@ | |||
|                       <a-popover v-if="restartResult" :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                         <span slot="title">{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span> | ||||
|                         <template slot="content"> | ||||
|                           <span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line ]]</span> | ||||
|                           <span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line | ||||
|                             ]]</span> | ||||
|                         </template> | ||||
|                         <a-icon type="question-circle"></a-icon> | ||||
|                       </a-popover> | ||||
|  | @ -537,6 +539,7 @@ | |||
|             serverObj = o.settings.vnext; | ||||
|             break; | ||||
|           case Protocols.VLESS: | ||||
|             return [o.settings?.address + ':' + o.settings?.port]; | ||||
|           case Protocols.HTTP: | ||||
|           case Protocols.Socks: | ||||
|           case Protocols.Shadowsocks: | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ var defaultValueMap = map[string]string{ | |||
| 	"secret":                      random.Seq(32), | ||||
| 	"webBasePath":                 "/", | ||||
| 	"sessionMaxAge":               "360", | ||||
| 	"pageSize":                    "50", | ||||
| 	"pageSize":                    "25", | ||||
| 	"expireDiff":                  "0", | ||||
| 	"trafficDiff":                 "0", | ||||
| 	"remarkModel":                 "-ieo", | ||||
|  |  | |||
|  | @ -46,22 +46,22 @@ var ( | |||
| 	hashStorage *global.HashStorage | ||||
| 
 | ||||
| 	// Performance improvements
 | ||||
| 	messageWorkerPool chan struct{} // Semaphore for limiting concurrent message processing
 | ||||
| 	optimizedHTTPClient *http.Client // HTTP client with connection pooling and timeouts
 | ||||
| 	 | ||||
| 	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
 | ||||
| 	receiver_inbound_ID int | ||||
| 	client_Id           string | ||||
|  | @ -122,7 +122,7 @@ func (t *Tgbot) GetHashStorage() *global.HashStorage { | |||
| 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 | ||||
| 	} | ||||
|  | @ -133,7 +133,7 @@ func (t *Tgbot) getCachedStatus() (*Status, bool) { | |||
| func (t *Tgbot) setCachedStatus(status *Status) { | ||||
| 	statusCache.mutex.Lock() | ||||
| 	defer statusCache.mutex.Unlock() | ||||
| 	 | ||||
| 
 | ||||
| 	statusCache.data = status | ||||
| 	statusCache.timestamp = time.Now() | ||||
| } | ||||
|  | @ -142,7 +142,7 @@ func (t *Tgbot) setCachedStatus(status *Status) { | |||
| 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 | ||||
| 	} | ||||
|  | @ -153,7 +153,7 @@ func (t *Tgbot) getCachedServerStats() (string, bool) { | |||
| func (t *Tgbot) setCachedServerStats(stats string) { | ||||
| 	serverStatsCache.mutex.Lock() | ||||
| 	defer serverStatsCache.mutex.Unlock() | ||||
| 	 | ||||
| 
 | ||||
| 	serverStatsCache.data = stats | ||||
| 	serverStatsCache.timestamp = time.Now() | ||||
| } | ||||
|  | @ -171,7 +171,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error { | |||
| 
 | ||||
| 	// 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, | ||||
|  | @ -359,9 +359,9 @@ func (t *Tgbot) OnReceive() { | |||
| 	botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { | ||||
| 		// Use goroutine with worker pool for concurrent command processing
 | ||||
| 		go func() { | ||||
| 			messageWorkerPool <- struct{}{} // Acquire worker
 | ||||
| 			messageWorkerPool <- struct{}{}        // Acquire worker
 | ||||
| 			defer func() { <-messageWorkerPool }() // Release worker
 | ||||
| 			 | ||||
| 
 | ||||
| 			delete(userStates, message.Chat.ID) | ||||
| 			t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID)) | ||||
| 		}() | ||||
|  | @ -371,9 +371,9 @@ func (t *Tgbot) OnReceive() { | |||
| 	botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error { | ||||
| 		// Use goroutine with worker pool for concurrent callback processing
 | ||||
| 		go func() { | ||||
| 			messageWorkerPool <- struct{}{} // Acquire worker
 | ||||
| 			messageWorkerPool <- struct{}{}        // Acquire worker
 | ||||
| 			defer func() { <-messageWorkerPool }() // Release worker
 | ||||
| 			 | ||||
| 
 | ||||
| 			delete(userStates, query.Message.GetChat().ID) | ||||
| 			t.answerCallback(&query, checkAdmin(query.From.ID)) | ||||
| 		}() | ||||
|  | @ -2537,7 +2537,7 @@ func (t *Tgbot) prepareServerUsageInfo() string { | |||
| 	if cachedStats, found := t.getCachedServerStats(); found { | ||||
| 		return cachedStats | ||||
| 	} | ||||
| 	 | ||||
| 
 | ||||
| 	info, ipv4, ipv6 := "", "", "" | ||||
| 
 | ||||
| 	// get latest status of server with caching
 | ||||
|  | @ -2588,10 +2588,10 @@ func (t *Tgbot) prepareServerUsageInfo() string { | |||
| 	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.xrayStatus", "State=="+fmt.Sprint(t.lastStatus.Xray.State)) | ||||
| 	 | ||||
| 
 | ||||
| 	// Cache the complete server stats
 | ||||
| 	t.setCachedServerStats(info) | ||||
| 	 | ||||
| 
 | ||||
| 	return info | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue