Browse Source

Major changes to tgbot, also small changes for panel (#1463)

* Reduce outage time on Xray errors

* Improved logs clearing, added previous logs
File name change: 3xipl-access-persistent.log -> 3xipl-ap.log
All previous logs have .prev suffix

* Preparations for tgbot additions

* [tgbot] Improvements, Additions and Fixes
* Changed interaction with Expire Date for Clients
* Added more info and interactions with Online Clients
* Added a way to get Ban Logs (also added them to backup)
* Few fixes and optimizations in code
* Fixed RU translation

* [tgbot] More updates and fixes

* [tgbot] Quick Fix

* [tgbot] Quick Fix 2

* [tgbot] Big Updates
Added Notifications for Clients throught Tgbot (when Expire)
Added compability for Usernames both w/wo @
Added more buttons overall for admins

* [tgbot] Fixes

* [tbot] Fixes 2

* [tgbot] Removed usernames support for Notifications to work

* [tgbot] Fix

* [tgbot] Fix Notify

* [tgbot] small fixes

* [tgbot] replyMarkup only for last message on big messages

* [tgbot] Fixed last message is empty

* [tgbot] Fix messages split
somebodywashere 1 year ago
parent
commit
ceee1e4277

+ 3 - 0
web/job/check_client_ip_job.go

@@ -22,8 +22,11 @@ var job *CheckClientIpJob
 var disAllowedIps []string
 var ipFiles = []string{
 	xray.GetIPLimitLogPath(),
+xray.GetIPLimitPrevLogPath(),
 	xray.GetIPLimitBannedLogPath(),
+xray.GetIPLimitBannedPrevLogPath(),
 	xray.GetAccessPersistentLogPath(),
+xray.GetAccessPersistentPrevLogPath(),
 }
 
 func NewCheckClientIpJob() *CheckClientIpJob {

+ 14 - 7
web/job/check_xray_running_job.go

@@ -1,6 +1,9 @@
 package job
 
-import "x-ui/web/service"
+import (
+	"x-ui/logger"
+	"x-ui/web/service"
+)
 
 type CheckXrayRunningJob struct {
 	xrayService service.XrayService
@@ -15,11 +18,15 @@ func NewCheckXrayRunningJob() *CheckXrayRunningJob {
 func (j *CheckXrayRunningJob) Run() {
 	if j.xrayService.IsXrayRunning() {
 		j.checkTime = 0
-		return
+	} else {
+		j.checkTime++
+		//only restart if it's down 2 times in a row
+		if j.checkTime > 1 {
+			err := j.xrayService.RestartXray(false)
+			j.checkTime = 0
+			if err != nil {
+				logger.Error("Restart xray failed:", err)
+			}
+		}
 	}
-	j.checkTime++
-	if j.checkTime < 2 {
-		return
-	}
-	j.xrayService.SetToNeedRestart()
 }

+ 29 - 2
web/job/clear_logs_job.go

@@ -15,10 +15,37 @@ func NewClearLogsJob() *ClearLogsJob {
 // Here Run is an interface method of the Job interface
 func (j *ClearLogsJob) Run() {
 	logFiles := []string{xray.GetIPLimitLogPath(), xray.GetIPLimitBannedLogPath(), xray.GetAccessPersistentLogPath()}
+	logFilesPrev := []string{xray.GetIPLimitPrevLogPath(), xray.GetIPLimitBannedPrevLogPath(), xray.GetAccessPersistentPrevLogPath()}
 
-	// clear log files
+	// clear old previous logs
+	for i := 0; i < len(logFilesPrev); i++ {
+		if err := os.Truncate(logFilesPrev[i], 0); err != nil {
+			logger.Warning("clear logs job err:", err)
+		}
+	}
+
+	// clear log files and copy to previous logs
 	for i := 0; i < len(logFiles); i++ {
-		if err := os.Truncate(logFiles[i], 0); err != nil {
+		
+		// copy to previous logs
+		logFilePrev, err := os.OpenFile(logFilesPrev[i], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
+		if err != nil {
+			logger.Warning("clear logs job err:", err)
+		}
+
+		logFile, err := os.ReadFile(logFiles[i])
+		if err != nil {
+			logger.Warning("clear logs job err:", err)
+		}
+
+		_, err = logFilePrev.Write(logFile)
+		if err != nil {
+			logger.Warning("clear logs job err:", err)
+		}
+		defer logFilePrev.Close()
+
+		err = os.Truncate(logFiles[i], 0)
+		if err != nil {
 			logger.Warning("clear logs job err:", err)
 		}
 	}

+ 39 - 3
web/service/inbound.go

@@ -1146,6 +1146,8 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) err
 		if oldClient.Email == clientEmail {
 			if inbound.Protocol == "trojan" {
 				clientId = oldClient.Password
+			} else if inbound.Protocol == "shadowsocks" {
+				clientId = oldClient.Email
 			} else {
 				clientId = oldClient.ID
 			}
@@ -1184,6 +1186,32 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) err
 	return nil
 }
 
+func (s *InboundService) checkIsEnabledByEmail(clientEmail string) (bool, error) {
+	_, inbound, err := s.GetClientInboundByEmail(clientEmail)
+	if err != nil {
+		return false, err
+	}
+	if inbound == nil {
+		return false, common.NewError("Inbound Not Found For Email:", clientEmail)
+	}
+
+	clients, err := s.GetClients(inbound)
+	if err != nil {
+		return false, err
+	}
+
+	isEnable := false
+
+	for _, client := range clients {
+		if client.Email == clientEmail {
+			isEnable = client.Enable
+			break
+		}
+	}
+
+	return isEnable, err
+}
+
 func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, error) {
 	_, inbound, err := s.GetClientInboundByEmail(clientEmail)
 	if err != nil {
@@ -1205,6 +1233,8 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, er
 		if oldClient.Email == clientEmail {
 			if inbound.Protocol == "trojan" {
 				clientId = oldClient.Password
+			} else if inbound.Protocol == "shadowsocks" {
+				clientId = oldClient.Email
 			} else {
 				clientId = oldClient.ID
 			}
@@ -1266,6 +1296,8 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
 		if oldClient.Email == clientEmail {
 			if inbound.Protocol == "trojan" {
 				clientId = oldClient.Password
+			} else if inbound.Protocol == "shadowsocks" {
+				clientId = oldClient.Email
 			} else {
 				clientId = oldClient.ID
 			}
@@ -1324,6 +1356,8 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
 		if oldClient.Email == clientEmail {
 			if inbound.Protocol == "trojan" {
 				clientId = oldClient.Password
+			} else if inbound.Protocol == "shadowsocks" {
+				clientId = oldClient.Email
 			} else {
 				clientId = oldClient.ID
 			}
@@ -1385,6 +1419,8 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
 		if oldClient.Email == clientEmail {
 			if inbound.Protocol == "trojan" {
 				clientId = oldClient.Password
+			} else if inbound.Protocol == "shadowsocks" {
+				clientId = oldClient.Email
 			} else {
 				clientId = oldClient.ID
 			}
@@ -1613,10 +1649,10 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
 	return nil
 }
 
-func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTraffic, error) {
+func (s *InboundService) GetClientTrafficTgBot(tgId string) ([]*xray.ClientTraffic, error) {
 	db := database.GetDB()
 	var inbounds []*model.Inbound
-	err := db.Model(model.Inbound{}).Where("settings like ?", fmt.Sprintf(`%%"tgId": "%s"%%`, tguname)).Find(&inbounds).Error
+	err := db.Model(model.Inbound{}).Where("settings like ?", fmt.Sprintf(`%%"tgId": "%s"%%`, tgId)).Find(&inbounds).Error
 	if err != nil && err != gorm.ErrRecordNotFound {
 		return nil, err
 	}
@@ -1627,7 +1663,7 @@ func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTr
 			logger.Error("Unable to get clients from inbound")
 		}
 		for _, client := range clients {
-			if client.TgID == tguname {
+			if client.TgID == tgId {
 				emails = append(emails, client.Email)
 			}
 		}

+ 407 - 291
web/service/tgbot.go

@@ -7,6 +7,7 @@ import (
 	"os"
 	"strconv"
 	"strings"
+	"slices"
 	"time"
 	"x-ui/config"
 	"x-ui/database"
@@ -218,7 +219,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
 			if isAdmin {
 				t.searchClient(chatId, commandArgs[0])
 			} else {
-				t.searchForClient(chatId, commandArgs[0])
+				t.getClientUsage(chatId, strconv.FormatInt(message.From.ID, 10), commandArgs[0])
 			}
 		} else {
 			msg += t.I18nBot("tgbot.commands.usage")
@@ -234,11 +235,14 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
 		msg += t.I18nBot("tgbot.commands.unknown")
 	}
 
-	if onlyMessage {
-		t.SendMsgToTgbot(chatId, msg)
-		return
+	if msg != ""{
+		if onlyMessage {
+			t.SendMsgToTgbot(chatId, msg)
+			return
+		} else {
+			t.SendAnswer(chatId, msg, isAdmin)
+		}
 	}
-	t.SendAnswer(chatId, msg, isAdmin)
 }
 
 func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {
@@ -257,6 +261,9 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 		if len(dataArray) >= 2 && len(dataArray[1]) > 0 {
 			email := dataArray[1]
 			switch dataArray[0] {
+			case "client_get_usage":
+				t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.messages.email", "Email=="+email))
+				t.searchClient(chatId, email)
 			case "client_refresh":
 				t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clientRefreshSuccess", "Email=="+email))
 				t.searchClient(chatId, email, callbackQuery.Message.MessageID)
@@ -352,7 +359,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 									inputNumber = 0
 								} else if num == -1 {
 									if inputNumber > 0 {
-										inputNumber = (inputNumber / 10) ^ 0
+										inputNumber = (inputNumber / 10)
 									}
 								} else {
 									inputNumber = (inputNumber * 10) + num
@@ -372,7 +379,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 								tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("client_cancel "+email)),
 							),
 							tu.InlineKeyboardRow(
-								tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmNumber", "Num=="+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" "+strconv.Itoa(inputNumber))),
+								tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmNumberAdd", "Num=="+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" "+strconv.Itoa(inputNumber))),
 							),
 							tu.InlineKeyboardRow(
 								tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 1")),
@@ -411,20 +418,20 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 						tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.custom")).WithCallbackData(t.encodeQuery("reset_exp_in "+email+" 0")),
 					),
 					tu.InlineKeyboardRow(
-						tu.InlineKeyboardButton("1 "+t.I18nBot("tgbot.month")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 30")),
-						tu.InlineKeyboardButton("2 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 60")),
+						tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 7 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 7")),
+						tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 10 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 10")),
 					),
 					tu.InlineKeyboardRow(
-						tu.InlineKeyboardButton("3 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 90")),
-						tu.InlineKeyboardButton("6 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 180")),
+						tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 14 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 14")),
+						tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 20 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 20")),
 					),
 					tu.InlineKeyboardRow(
-						tu.InlineKeyboardButton("9 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 270")),
-						tu.InlineKeyboardButton("12 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 360")),
+						tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 1 "+t.I18nBot("tgbot.month")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 30")),
+						tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 3 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 90")),
 					),
 					tu.InlineKeyboardRow(
-						tu.InlineKeyboardButton("10 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 10")),
-						tu.InlineKeyboardButton("20 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 20")),
+						tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 6 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 180")),
+						tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 12 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 365")),
 					),
 				)
 				t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
@@ -434,7 +441,29 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 					if err == nil {
 						var date int64 = 0
 						if days > 0 {
-							date = int64(-(days * 24 * 60 * 60000))
+							traffic, err := t.inboundService.GetClientTrafficByEmail(email)
+							if err != nil {
+								logger.Warning(err)
+								msg := t.I18nBot("tgbot.wentWrong")
+								t.SendMsgToTgbot(chatId, msg)
+								return
+							}
+							if traffic == nil {
+								msg := t.I18nBot("tgbot.noResult")
+								t.SendMsgToTgbot(chatId, msg)
+								return
+							}
+
+							if traffic.ExpiryTime > 0 {
+								if traffic.ExpiryTime-time.Now().Unix()*1000 < 0 {
+									date = -int64(days * 24 * 60 * 60000)
+								} else {
+									date = traffic.ExpiryTime + int64(days*24*60*60000)
+								}
+							} else {
+								date = traffic.ExpiryTime - int64(days*24*60*60000)
+							}
+
 						}
 						err := t.inboundService.ResetClientExpiryTimeByEmail(email, date)
 						if err == nil {
@@ -459,7 +488,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 									inputNumber = 0
 								} else if num == -1 {
 									if inputNumber > 0 {
-										inputNumber = (inputNumber / 10) ^ 0
+										inputNumber = (inputNumber / 10)
 									}
 								} else {
 									inputNumber = (inputNumber * 10) + num
@@ -564,7 +593,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 									inputNumber = 0
 								} else if num == -1 {
 									if inputNumber > 0 {
-										inputNumber = (inputNumber / 10) ^ 0
+										inputNumber = (inputNumber / 10)
 									}
 								} else {
 									inputNumber = (inputNumber * 10) + num
@@ -661,6 +690,16 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 					t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
 				}
 			case "toggle_enable":
+				inlineKeyboard := tu.InlineKeyboard(
+					tu.InlineKeyboardRow(
+						tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("client_cancel "+email)),
+					),
+					tu.InlineKeyboardRow(
+						tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmToggle")).WithCallbackData(t.encodeQuery("toggle_enable_c "+email)),
+					),
+				)
+				t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
+			case "toggle_enable_c":
 				enabled, err := t.inboundService.ToggleClientEnableByEmail(email)
 				if err == nil {
 					t.xrayService.SetToNeedRestart()
@@ -678,24 +717,36 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 		}
 	}
 
-	// Respond to the callback query, telling Telegram to show the user
-	// a message with the data received.
-	t.sendCallbackAnswerTgBot(callbackQuery.ID, callbackQuery.Data)
-
 	switch callbackQuery.Data {
 	case "get_usage":
+		t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.serverUsage"))
 		t.SendMsgToTgbot(chatId, t.getServerUsage())
 	case "inbounds":
+		t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.getInbounds"))
 		t.SendMsgToTgbot(chatId, t.getInboundUsages())
 	case "deplete_soon":
-		t.SendMsgToTgbot(chatId, t.getExhausted())
+		t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.depleteSoon"))
+		t.getExhausted(chatId)
 	case "get_backup":
+		t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.dbBackup"))
 		t.sendBackup(chatId)
+	case "get_banlogs":
+		t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.getBanLogs"))
+		t.sendBanLogs(chatId, true)
 	case "client_traffic":
-		t.getClientUsage(chatId, callbackQuery.From.Username, strconv.FormatInt(callbackQuery.From.ID, 10))
+		t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.clientUsage"))
+		t.getClientUsage(chatId, strconv.FormatInt(callbackQuery.From.ID, 10))
 	case "client_commands":
+		t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands"))
 		t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpClientCommands"))
+	case "onlines":
+		t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.onlines"))
+		t.onlineClients(chatId)
+	case "onlines_refresh":
+		t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
+		t.onlineClients(chatId, callbackQuery.Message.MessageID)
 	case "commands":
+		t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands"))
 		t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpAdminCommands"))
 	}
 }
@@ -713,7 +764,10 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
 	numericKeyboard := tu.InlineKeyboard(
 		tu.InlineKeyboardRow(
 			tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.serverUsage")).WithCallbackData(t.encodeQuery("get_usage")),
+		),
+		tu.InlineKeyboardRow(
 			tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.dbBackup")).WithCallbackData(t.encodeQuery("get_backup")),
+			tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.getBanLogs")).WithCallbackData(t.encodeQuery("get_banlogs")),
 		),
 		tu.InlineKeyboardRow(
 			tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.getInbounds")).WithCallbackData(t.encodeQuery("inbounds")),
@@ -721,6 +775,7 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
 		),
 		tu.InlineKeyboardRow(
 			tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.commands")).WithCallbackData(t.encodeQuery("commands")),
+			tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.onlines")).WithCallbackData(t.encodeQuery("onlines")),
 		),
 	)
 	numericKeyboardClient := tu.InlineKeyboard(
@@ -754,7 +809,7 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
 
 	// paging message if it is big
 	if len(msg) > limit {
-		messages := strings.Split(msg, "\r\n \r\n")
+		messages := strings.Split(msg, "\r\n\r\n")
 		lastIndex := -1
 
 		for _, message := range messages {
@@ -762,19 +817,23 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
 				allMessages = append(allMessages, message)
 				lastIndex++
 			} else {
-				allMessages[lastIndex] += "\r\n \r\n" + message
+				allMessages[lastIndex] += "\r\n\r\n" + message
 			}
 		}
+		if strings.TrimSpace(allMessages[len(allMessages)-1]) == "" {
+			allMessages = allMessages[:len(allMessages)-1]
+		}
 	} else {
 		allMessages = append(allMessages, msg)
 	}
-	for _, message := range allMessages {
+	for n, message := range allMessages {
 		params := telego.SendMessageParams{
 			ChatID:    tu.ID(chatId),
 			Text:      message,
 			ParseMode: "HTML",
 		}
-		if len(replyMarkup) > 0 {
+		//only add replyMarkup to last message
+		if len(replyMarkup) > 0 && n == (len(allMessages)-1) {
 			params.ReplyMarkup = replyMarkup[0]
 		}
 		_, err := bot.SendMessage(&params)
@@ -785,9 +844,15 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
 	}
 }
 
-func (t *Tgbot) SendMsgToTgbotAdmins(msg string) {
-	for _, adminId := range adminIds {
-		t.SendMsgToTgbot(adminId, msg)
+func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMarkup) {
+	if len(replyMarkup) > 0 {
+		for _, adminId := range adminIds {
+			t.SendMsgToTgbot(adminId, msg, replyMarkup[0])
+		}
+	} else {
+		for _, adminId := range adminIds {
+			t.SendMsgToTgbot(adminId, msg)
+		}
 	}
 }
 
@@ -803,8 +868,8 @@ func (t *Tgbot) SendReport() {
 	info := t.getServerUsage()
 	t.SendMsgToTgbotAdmins(info)
 
-	exhausted := t.getExhausted()
-	t.SendMsgToTgbotAdmins(exhausted)
+	t.sendExhaustedToAdmins()
+	t.notifyExhausted()
 
 	backupEnable, err := t.settingService.GetTgBotBackup()
 	if err == nil && backupEnable {
@@ -821,6 +886,15 @@ func (t *Tgbot) SendBackupToAdmins() {
 	}
 }
 
+func (t *Tgbot) sendExhaustedToAdmins() {
+	if !t.IsRunning() {
+		return
+	}
+	for _, adminId := range adminIds {
+		t.getExhausted(int64(adminId))
+	}
+}
+
 func (t *Tgbot) getServerUsage() string {
 	info, ipv4, ipv6 := "", "", ""
 	info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
@@ -831,7 +905,7 @@ func (t *Tgbot) getServerUsage() string {
 	if err != nil {
 		logger.Error("net.Interfaces failed, err: ", err.Error())
 		info += t.I18nBot("tgbot.messages.ip", "IP=="+t.I18nBot("tgbot.unknown"))
-		info += " \r\n"
+		info += "\r\n"
 	} else {
 		for i := 0; i < len(netInterfaces); i++ {
 			if (netInterfaces[i].Flags & net.FlagUp) != 0 {
@@ -855,9 +929,11 @@ func (t *Tgbot) getServerUsage() string {
 
 	// get latest status of server
 	t.lastStatus = t.serverService.GetStatus(t.lastStatus)
+	onlines := p.GetOnlineClients()
 	info += t.I18nBot("tgbot.messages.serverUpTime", "UpTime=="+strconv.FormatUint(t.lastStatus.Uptime/86400, 10), "Unit=="+t.I18nBot("tgbot.days"))
 	info += t.I18nBot("tgbot.messages.serverLoad", "Load1=="+strconv.FormatFloat(t.lastStatus.Loads[0], 'f', 2, 64), "Load2=="+strconv.FormatFloat(t.lastStatus.Loads[1], 'f', 2, 64), "Load3=="+strconv.FormatFloat(t.lastStatus.Loads[2], 'f', 2, 64))
 	info += t.I18nBot("tgbot.messages.serverMemory", "Current=="+common.FormatTraffic(int64(t.lastStatus.Mem.Current)), "Total=="+common.FormatTraffic(int64(t.lastStatus.Mem.Total)))
+	info += t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(len(onlines)))
 	info += t.I18nBot("tgbot.messages.tcpCount", "Count=="+strconv.Itoa(t.lastStatus.TcpCount))
 	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)))
@@ -914,85 +990,136 @@ func (t *Tgbot) getInboundUsages() string {
 			} else {
 				info += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
 			}
+			info += "\r\n"
 		}
 	}
 	return info
 }
 
-func (t *Tgbot) getClientUsage(chatId int64, tgUserName string, tgUserID string) {
-	traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
+func (t *Tgbot) clientInfoMsg(traffic *xray.ClientTraffic, printEnabled bool, printOnline bool, printActive bool,
+								printDate bool, printTraffic bool, printRefreshed bool) string {
+
+	now := time.Now().Unix()
+	expiryTime := ""
+	flag := false
+	diff := traffic.ExpiryTime/1000 - now
+	if traffic.ExpiryTime == 0 {
+		expiryTime = t.I18nBot("tgbot.unlimited")
+	} else if diff > 172800 || !traffic.Enable {
+		expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
+	} else if traffic.ExpiryTime < 0 {
+		expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
+		flag = true
+	} else {
+		expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
+		flag = true
+	}
+
+	total := ""
+	if traffic.Total == 0 {
+		total = t.I18nBot("tgbot.unlimited")
+	} else {
+		total = common.FormatTraffic((traffic.Total))
+	}
+
+	enabled := ""
+	isEnabled, err := t.inboundService.checkIsEnabledByEmail(traffic.Email)
 	if err != nil {
 		logger.Warning(err)
-		msg := t.I18nBot("tgbot.wentWrong")
-		t.SendMsgToTgbot(chatId, msg)
-		return
+		enabled = t.I18nBot("tgbot.wentWrong")
+	} else if isEnabled {
+		enabled = t.I18nBot("tgbot.messages.yes")
+	} else {
+		enabled = t.I18nBot("tgbot.messages.no")
 	}
 
-	if len(traffics) == 0 {
-		if len(tgUserName) == 0 {
-			msg := t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+tgUserID)
-			t.SendMsgToTgbot(chatId, msg)
-			return
+	active := ""
+	if traffic.Enable {
+		active = t.I18nBot("tgbot.messages.yes")
+	} else {
+		active = t.I18nBot("tgbot.messages.no")
+	}
+
+	status := t.I18nBot("tgbot.offline")
+	if p.IsRunning() {
+		for _, online := range p.GetOnlineClients() {
+			if online == traffic.Email {
+				status = t.I18nBot("tgbot.online")
+				break
+			}
+		}
+	}
+
+	output := ""
+	output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
+	if printEnabled {
+		output += t.I18nBot("tgbot.messages.enabled", "Enable=="+enabled)
+	}
+	if printOnline {
+		output += t.I18nBot("tgbot.messages.online", "Status=="+status)
+	}
+	if printActive {
+		output += t.I18nBot("tgbot.messages.active", "Enable=="+active)
+	}
+	if printDate {
+		if flag {
+			output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
+		} else {
+			output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
 		}
-		traffics, err = t.inboundService.GetClientTrafficTgBot(tgUserName)
 	}
+	if printTraffic {
+		output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
+		output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
+		output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
+	}
+	if printRefreshed {
+		output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
+	}
+
+	return output
+}
+
+func (t *Tgbot) getClientUsage(chatId int64, tgUserID string, email ...string) {
+	traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
 	if err != nil {
 		logger.Warning(err)
 		msg := t.I18nBot("tgbot.wentWrong")
 		t.SendMsgToTgbot(chatId, msg)
 		return
 	}
+
 	if len(traffics) == 0 {
-		msg := t.I18nBot("tgbot.answers.askToAddUserName", "TgUserName=="+tgUserName, "TgUserID=="+tgUserID)
-		t.SendMsgToTgbot(chatId, msg)
+		t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+tgUserID))
 		return
 	}
 
-	now := time.Now().Unix()
-	for _, traffic := range traffics {
-		expiryTime := ""
-		flag := false
-		diff := traffic.ExpiryTime/1000 - now
-		if traffic.ExpiryTime == 0 {
-			expiryTime = t.I18nBot("tgbot.unlimited")
-		} else if diff > 172800 || !traffic.Enable {
-			expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
-		} else if traffic.ExpiryTime < 0 {
-			expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
-			flag = true
-		} else {
-			expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
-			flag = true
-		}
-
-		total := ""
-		if traffic.Total == 0 {
-			total = t.I18nBot("tgbot.unlimited")
-		} else {
-			total = common.FormatTraffic((traffic.Total))
-		}
+	output := ""
 
-		output := ""
-		output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
-		if traffic.Enable {
-			output += t.I18nBot("tgbot.messages.active")
-			if flag {
-				output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
-			} else {
-				output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
+	if len(traffics) > 0 {
+		if len(email) > 0 {
+			for _, traffic := range traffics {
+				if traffic.Email == email[0] {
+					output := t.clientInfoMsg(traffic, true, true, true, true, true, true)
+					t.SendMsgToTgbot(chatId, output)
+					return
+				}
 			}
+			msg := t.I18nBot("tgbot.noResult")
+			t.SendMsgToTgbot(chatId, msg)
+			return
 		} else {
-			output += t.I18nBot("tgbot.messages.inactive")
-			output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
+			for _, traffic := range traffics {
+				output += t.clientInfoMsg(traffic, true, true, true, true, true, false)
+				output += "\r\n"
+			}
 		}
-		output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
-		output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
-		output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
-		output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
-
-		t.SendMsgToTgbot(chatId, output)
 	}
-	t.SendAnswer(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), false)
+
+	output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
+	t.SendMsgToTgbot(chatId, output)
+	output = t.I18nBot("tgbot.commands.pleaseChoose")
+	t.SendAnswer(chatId, output, false)
 }
 
 func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
@@ -1088,46 +1215,7 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
 		return
 	}
 
-	now := time.Now().Unix()
-	expiryTime := ""
-	flag := false
-	diff := traffic.ExpiryTime/1000 - now
-	if traffic.ExpiryTime == 0 {
-		expiryTime = t.I18nBot("tgbot.unlimited")
-	} else if diff > 172800 || !traffic.Enable {
-		expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
-	} else if traffic.ExpiryTime < 0 {
-		expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
-		flag = true
-	} else {
-		expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
-		flag = true
-	}
-
-	total := ""
-	if traffic.Total == 0 {
-		total = t.I18nBot("tgbot.unlimited")
-	} else {
-		total = common.FormatTraffic((traffic.Total))
-	}
-
-	output := ""
-	output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
-	if traffic.Enable {
-		output += t.I18nBot("tgbot.messages.active")
-		if flag {
-			output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
-		} else {
-			output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
-		}
-	} else {
-		output += t.I18nBot("tgbot.messages.inactive")
-		output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
-	}
-	output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
-	output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
-	output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
-	output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
+	output := t.clientInfoMsg(traffic, true, true, true, true, true, true)
 
 	inlineKeyboard := tu.InlineKeyboard(
 		tu.InlineKeyboardRow(
@@ -1151,7 +1239,6 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
 			tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.toggle")).WithCallbackData(t.encodeQuery("toggle_enable "+email)),
 		),
 	)
-
 	if len(messageID) > 0 {
 		t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard)
 	} else {
@@ -1173,7 +1260,6 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
 		return
 	}
 
-	now := time.Now().Unix()
 	for _, inbound := range inbouds {
 		info := ""
 		info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)
@@ -1187,111 +1273,17 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
 		}
 		t.SendMsgToTgbot(chatId, info)
 
-		for _, traffic := range inbound.ClientStats {
-			expiryTime := ""
-			flag := false
-			diff := traffic.ExpiryTime/1000 - now
-			if traffic.ExpiryTime == 0 {
-				expiryTime = t.I18nBot("tgbot.unlimited")
-			} else if diff > 172800 || !traffic.Enable {
-				expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
-			} else if traffic.ExpiryTime < 0 {
-				expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
-				flag = true
-			} else {
-				expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
-				flag = true
-			}
-
-			total := ""
-			if traffic.Total == 0 {
-				total = t.I18nBot("tgbot.unlimited")
-			} else {
-				total = common.FormatTraffic((traffic.Total))
-			}
-
+		if len(inbound.ClientStats) > 0 {
 			output := ""
-			output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
-			if traffic.Enable {
-				output += t.I18nBot("tgbot.messages.active")
-				if flag {
-					output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
-				} else {
-					output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
-				}
-			} else {
-				output += t.I18nBot("tgbot.messages.inactive")
-				output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
+			for _, traffic := range inbound.ClientStats {
+				output += t.clientInfoMsg(&traffic, true, true, true, true, true, true)
 			}
-			output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
-			output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
-			output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
-			output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
-
 			t.SendMsgToTgbot(chatId, output)
 		}
 	}
 }
 
-func (t *Tgbot) searchForClient(chatId int64, query string) {
-	traffic, err := t.inboundService.SearchClientTraffic(query)
-	if err != nil {
-		logger.Warning(err)
-		msg := t.I18nBot("tgbot.wentWrong")
-		t.SendMsgToTgbot(chatId, msg)
-		return
-	}
-	if traffic == nil {
-		msg := t.I18nBot("tgbot.noResult")
-		t.SendMsgToTgbot(chatId, msg)
-		return
-	}
-
-	now := time.Now().Unix()
-	expiryTime := ""
-	flag := false
-	diff := traffic.ExpiryTime/1000 - now
-	if traffic.ExpiryTime == 0 {
-		expiryTime = t.I18nBot("tgbot.unlimited")
-	} else if diff > 172800 || !traffic.Enable {
-		expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
-	} else if traffic.ExpiryTime < 0 {
-		expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
-		flag = true
-	} else {
-		expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
-		flag = true
-	}
-
-	total := ""
-	if traffic.Total == 0 {
-		total = t.I18nBot("tgbot.unlimited")
-	} else {
-		total = common.FormatTraffic((traffic.Total))
-	}
-
-	output := ""
-	output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
-	if traffic.Enable {
-		output += t.I18nBot("tgbot.messages.active")
-		if flag {
-			output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
-		} else {
-			output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
-		}
-	} else {
-		output += t.I18nBot("tgbot.messages.inactive")
-		output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
-	}
-	output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
-	output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
-	output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
-	output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
-
-	t.SendMsgToTgbot(chatId, output)
-}
-
-func (t *Tgbot) getExhausted() string {
+func (t *Tgbot) getExhausted(chatId int64) {
 	trDiff := int64(0)
 	exDiff := int64(0)
 	now := time.Now().Unix() * 1000
@@ -1341,10 +1333,9 @@ func (t *Tgbot) getExhausted() string {
 	output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.inbounds"))
 	output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledInbounds)))
 	output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedInbounds)))
-	output += "\r\n \r\n"
 
 	if len(exhaustedInbounds) > 0 {
-		output += t.I18nBot("tgbot.messages.exhaustedMsg", "Type=="+t.I18nBot("tgbot.inbounds"))
+		output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.inbounds"))
 
 		for _, inbound := range exhaustedInbounds {
 			output += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)
@@ -1355,63 +1346,148 @@ func (t *Tgbot) getExhausted() string {
 			} else {
 				output += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
 			}
-			output += "\r\n \r\n"
+			output += "\r\n"
 		}
 	}
 
 	// Clients
+	exhaustedCC := len(exhaustedClients)
 	output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients"))
 	output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients)))
-	output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedClients)))
-	output += "\r\n \r\n"
-
-	if len(exhaustedClients) > 0 {
-		output += t.I18nBot("tgbot.messages.exhaustedMsg", "Type=="+t.I18nBot("tgbot.clients"))
+	output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(exhaustedCC))
+	
 
+	if exhaustedCC > 0 {
+		output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.clients"))
+		var buttons []telego.InlineKeyboardButton
 		for _, traffic := range exhaustedClients {
-			expiryTime := ""
-			flag := false
-			diff := (traffic.ExpiryTime - now) / 1000
-			if traffic.ExpiryTime == 0 {
-				expiryTime = t.I18nBot("tgbot.unlimited")
-			} else if diff > 172800 || !traffic.Enable {
-				expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
-			} else if traffic.ExpiryTime < 0 {
-				expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
-				flag = true
-			} else {
-				expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
-				flag = true
-			}
+			output += t.clientInfoMsg(&traffic, true, false, false, true, true, false)
+			output += "\r\n"
+			buttons = append(buttons, tu.InlineKeyboardButton(traffic.Email).WithCallbackData(t.encodeQuery("client_get_usage "+traffic.Email)))
+		}
+		cols := 0
+		if exhaustedCC < 11 {
+			cols = 1
+		} else {
+			cols = 2
+		}
+		output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
+		keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))
+		t.SendMsgToTgbot(chatId, output, keyboard)
+	} else {
+		output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
+		t.SendMsgToTgbot(chatId, output)
+	}
+}
 
-			total := ""
-			if traffic.Total == 0 {
-				total = t.I18nBot("tgbot.unlimited")
-			} else {
-				total = common.FormatTraffic((traffic.Total))
-			}
+func (t *Tgbot) notifyExhausted() {
+	trDiff := int64(0)
+	exDiff := int64(0)
+	now := time.Now().Unix() * 1000
 
-			output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
-			if traffic.Enable {
-				output += t.I18nBot("tgbot.messages.active")
-				if flag {
-					output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
-				} else {
-					output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
+	TrafficThreshold, err := t.settingService.GetTrafficDiff()
+	if err == nil && TrafficThreshold > 0 {
+		trDiff = int64(TrafficThreshold) * 1073741824
+	}
+	ExpireThreshold, err := t.settingService.GetExpireDiff()
+	if err == nil && ExpireThreshold > 0 {
+		exDiff = int64(ExpireThreshold) * 86400000
+	}
+	inbounds, err := t.inboundService.GetAllInbounds()
+	if err != nil {
+		logger.Warning("Unable to load Inbounds", err)
+	}
+
+	var chatIDsDone []string
+	for _, inbound := range inbounds {
+		if inbound.Enable {
+			if len(inbound.ClientStats) > 0 {
+				clients, err := t.inboundService.GetClients(inbound)
+				if err == nil {
+					for _, client := range clients {
+						if client.TgID != "" {
+							chatID, err := strconv.ParseInt(client.TgID, 10, 64)
+							if err != nil {
+								logger.Warning("TgID is not a number: ", client.TgID)
+								continue
+							}
+							if !slices.Contains(chatIDsDone, client.TgID) && !checkAdmin(chatID) {
+								var disabledClients []xray.ClientTraffic
+								var exhaustedClients []xray.ClientTraffic
+								traffics, err := t.inboundService.GetClientTrafficTgBot(client.TgID)
+								if err == nil {
+									output := t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients"))
+									for _, traffic := range traffics {
+										if traffic.Enable {
+											if (traffic.ExpiryTime > 0 && (traffic.ExpiryTime-now < exDiff)) ||
+												(traffic.Total > 0 && (traffic.Total-(traffic.Up+traffic.Down) < trDiff)) {
+												exhaustedClients = append(exhaustedClients, *traffic)
+											}
+										} else {
+											disabledClients = append(disabledClients, *traffic)
+										}
+									}
+									if len(exhaustedClients) > 0 {
+										output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients)))
+										if len(disabledClients) > 0 {
+											output += t.I18nBot("tgbot.clients") + ":\r\n"
+											for _, traffic := range disabledClients {
+												output += " " + traffic.Email
+											}
+											output += "\r\n"
+										}
+										output += "\r\n"
+										output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedClients)))
+										for _, traffic := range exhaustedClients {
+											output += t.clientInfoMsg(&traffic, true, false, false, true, true, false)
+											output += "\r\n"
+										}
+										t.SendMsgToTgbot(chatID, output)
+									}
+									chatIDsDone = append(chatIDsDone, client.TgID)
+								}
+							}
+						}
+					}
 				}
-			} else {
-				output += t.I18nBot("tgbot.messages.inactive")
-				output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
 			}
-			output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
-			output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
-			output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
-			output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
-			output += "\r\n \r\n"
 		}
 	}
+}
 
-	return output
+func (t *Tgbot) onlineClients(chatId int64, messageID ...int) {
+	if !p.IsRunning() {
+		return
+	}
+
+	onlines := p.GetOnlineClients()
+	onlinesCount := len(onlines)
+	output := t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(onlinesCount))
+	keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow(
+		tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("onlines_refresh"))))
+	
+
+	if onlinesCount > 0 {
+		var buttons []telego.InlineKeyboardButton
+		for _, online := range onlines {
+			buttons = append(buttons, tu.InlineKeyboardButton(online).WithCallbackData(t.encodeQuery("client_get_usage "+online)))
+		}
+		cols := 0
+		if onlinesCount < 21 {
+			cols = 2
+		} else if onlinesCount < 61 {
+			cols = 3
+		} else {
+			cols = 4
+		}
+		keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, tu.InlineKeyboardCols(cols, buttons...)...)
+	}
+
+	if len(messageID) > 0 {
+		t.editMessageTgBot(chatId, messageID[0], output, keyboard)
+	} else {
+		t.SendMsgToTgbot(chatId, output, keyboard)
+	}
 }
 
 func (t *Tgbot) sendBackup(chatId int64) {
@@ -1421,33 +1497,73 @@ func (t *Tgbot) sendBackup(chatId int64) {
 	// Update by manually trigger a checkpoint operation
 	err := database.Checkpoint()
 	if err != nil {
-		logger.Warning("Error in trigger a checkpoint operation: ", err)
+		logger.Error("Error in trigger a checkpoint operation: ", err)
 	}
 
 	file, err := os.Open(config.GetDBPath())
-	if err != nil {
-		logger.Warning("Error in opening db file for backup: ", err)
-	}
-	document := tu.Document(
-		tu.ID(chatId),
-		tu.File(file),
-	)
-	_, err = bot.SendDocument(document)
-	if err != nil {
-		logger.Warning("Error in uploading backup: ", err)
+	if err == nil {
+		document := tu.Document(
+			tu.ID(chatId),
+			tu.File(file),
+		)
+		_, err = bot.SendDocument(document)
+		if err != nil {
+			logger.Error("Error in uploading backup: ", err)
+		}
+	} else {
+		logger.Error("Error in opening db file for backup: ", err)
+
 	}
 
 	file, err = os.Open(xray.GetConfigPath())
-	if err != nil {
-		logger.Warning("Error in opening config.json file for backup: ", err)
+	if err == nil {
+		document := tu.Document(
+			tu.ID(chatId),
+			tu.File(file),
+		)
+		_, err = bot.SendDocument(document)
+		if err != nil {
+			logger.Error("Error in uploading config.json: ", err)
+		}
+	} else {
+		logger.Error("Error in opening config.json file for backup: ", err)
 	}
-	document = tu.Document(
-		tu.ID(chatId),
-		tu.File(file),
-	)
-	_, err = bot.SendDocument(document)
-	if err != nil {
-		logger.Warning("Error in uploading config.json: ", err)
+
+	t.sendBanLogs(chatId, false)
+}
+
+func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
+	if dt {
+		output := t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05"))
+		t.SendMsgToTgbot(chatId, output)
+	}
+
+	file, err := os.Open(xray.GetIPLimitBannedPrevLogPath())
+	if err == nil {
+		document := tu.Document(
+			tu.ID(chatId),
+			tu.File(file),
+		)
+		_, err = bot.SendDocument(document)
+		if err != nil {
+			logger.Error("Error in uploading backup: ", err)
+		}
+	} else {
+		logger.Error("Error in opening db file for backup: ", err)
+	}
+
+	file, err = os.Open(xray.GetIPLimitBannedLogPath())
+	if err == nil {
+		document := tu.Document(
+			tu.ID(chatId),
+			tu.File(file),
+		)
+		_, err = bot.SendDocument(document)
+		if err != nil {
+			logger.Error("Error in uploading config.json: ", err)
+		}
+	} else {
+		logger.Error("Error in opening config.json file for backup: ", err)
 	}
 }
 

+ 1 - 1
web/service/xray.go

@@ -185,7 +185,7 @@ func (s *XrayService) RestartXray(isForce bool) error {
 		return err
 	}
 
-	if p != nil && p.IsRunning() {
+	if s.IsXrayRunning() {
 		if !isForce && p.GetConfig().Equals(xrayConfig) {
 			logger.Debug("It does not need to restart xray")
 			return nil

+ 22 - 12
web/translation/translate.en_US.toml

@@ -173,7 +173,7 @@
 "setDefaultCert" = "Set Cert from Panel"
 "xtlsDesc" = "Xray core needs to be 1.7.5"
 "realityDesc" = "Xray core needs to be 1.8.0 or higher."
-"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot or use '/id' command in bot )"
+"telegramDesc" = "Only use Chat ID (you can get it here @userinfobot or use '/id' command in bot)"
 "subscriptionDesc" = "you can find your sub link on Details, also you can use the same name for several configurations"
 "info" = "Info"
 "same" = "Same"
@@ -439,6 +439,7 @@
 "noIpRecord" = "❗ No IP Record!"
 "noInbounds" = "❗ No inbound found!"
 "unlimited" = "♾ Unlimited"
+"add" = "Add"
 "month" = "Month"
 "months" = "Months"
 "day" = "Day"
@@ -447,6 +448,8 @@
 "unknown" = "Unknown"
 "inbounds" = "Inbounds"
 "clients" = "Clients"
+"offline" = "🔴 Offline"
+"online" = "🟢 Online"
 
 [tgbot.commands]
 "unknown" = "❗ Unknown command"
@@ -457,8 +460,8 @@
 "status" = "✅ Bot is OK!"
 "usage" = "❗ Please provide a text to search!"
 "getID" = "🆔 Your ID: <code>{{ .ID }}</code>"
-"helpAdminCommands" = "Search for a client email:\r\n<code>/usage [Email]</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>"
-"helpClientCommands" = "To search for statistics, just use the following command:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nUse UUID for vmess/vless and Password for Trojan."
+"helpAdminCommands" = "Search for a client email:\r\n<code>/usage [Email]</code>\r\n\r\nSearch for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>"
+"helpClientCommands" = "To search for statistics, just use the following command:\r\n\r\n<code>/usage [Email]</code>"
 
 [tgbot.messages]
 "cpuThreshold" = "🔴 CPU Load {{ .Percent }}% is more than threshold {{ .Threshold }}%"
@@ -473,7 +476,7 @@
 "ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
 "ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
 "ip" = "🌐 IP: {{ .IP }}\r\n"
-"ips" = "🔢 IPs: \r\n{{ .IPs }}\r\n"
+"ips" = "🔢 IPs:\r\n{{ .IPs }}\r\n"
 "serverUpTime" = "⏳ Server Uptime: {{ .UpTime }} {{ .Unit }}\r\n"
 "serverLoad" = "📈 Server Load: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
 "serverMemory" = "📋 Server RAM: {{ .Current }}/{{ .Total }}\r\n"
@@ -487,8 +490,9 @@
 "port" = "🔌 Port: {{ .Port }}\r\n"
 "expire" = "📅 Expire Date: {{ .Time }}\r\n"
 "expireIn" = "📅 Expire In: {{ .Time }}\r\n"
-"active" = "💡 Active: ✅ Yes\r\n"
-"inactive" = "💡 Active: ❌ No\r\n"
+"active" = "💡 Active: {{ .Enable }}\r\n"
+"enabled" = "🚨 Enabled: {{ .Enable }}\r\n"
+"online" = "🌐 Connection status: {{ .Status }}\r\n"
 "email" = "📧 Email: {{ .Email }}\r\n"
 "upload" = "🔼 Upload: ↑{{ .Upload }}\r\n"
 "download" = "🔽 Download: ↓{{ .Download }}\r\n"
@@ -496,10 +500,13 @@
 "TGUser" = "👤 Telegram User: {{ .TelegramID }}\r\n"
 "exhaustedMsg" = "🚨 Exhausted {{ .Type }}:\r\n"
 "exhaustedCount" = "🚨 Exhausted {{ .Type }} count:\r\n"
+"onlinesCount" = "🌐 Online clients: {{ .Count }}\r\n"
 "disabled" = "🛑 Disabled: {{ .Disabled }}\r\n"
-"depleteSoon" = "🔜 Deplete Soon: {{ .Deplete }}\r\n \r\n"
+"depleteSoon" = "🔜 Deplete Soon: {{ .Deplete }}\r\n\r\n"
 "backupTime" = "🗄 Backup Time: {{ .Time }}\r\n"
-"refreshedOn" = "\r\n📋🔄 Refreshed On: {{ .Time }}\r\n \r\n"
+"refreshedOn" = "\r\n📋🔄 Refreshed On: {{ .Time }}\r\n\r\n"
+"yes" = "✅ Yes"
+"no" = "❌ No"
 
 [tgbot.buttons]
 "closeKeyboard" = "❌ Close Keyboard"
@@ -509,11 +516,13 @@
 "confirmResetTraffic" = "✅ Confirm Reset Traffic?"
 "confirmClearIps" = "✅ Confirm Clear IPs?"
 "confirmRemoveTGUser" = "✅ Confirm Remove Telegram User?"
+"confirmToggle" = "✅ Confirm Enable/Disable User?"
 "dbBackup" = "Get DB Backup"
 "serverUsage" = "Server Usage"
 "getInbounds" = "Get Inbounds"
 "depleteSoon" = "Deplete soon"
 "clientUsage" = "Get Usage"
+"onlines" = "Online Clients"
 "commands" = "Commands"
 "refresh" = "🔄 Refresh"
 "clearIPs" = "❌ Clear IPs"
@@ -521,14 +530,16 @@
 "selectTGUser" = "👤 Select Telegram User"
 "selectOneTGUser" = "👤 Select a telegram user:"
 "resetTraffic" = "📈 Reset Traffic"
-"resetExpire" = "📅 Reset Expire Days"
+"resetExpire" = "📅 Change Expiration Date"
 "ipLog" = "🔢 IP Log"
 "ipLimit" = "🔢 IP Limit"
 "setTGUser" = "👤 Set Telegram User"
 "toggle" = "🔘 Enable / Disable"
 "custom" = "🔢 Custom"
-"confirmNumber" = "✅ Confirm : {{ .Num }}"
+"confirmNumber" = "✅ Confirm: {{ .Num }}"
+"confirmNumberAdd" = "✅ Confirm adding: {{ .Num }}"
 "limitTraffic" = "🚧 Traffic Limit"
+"getBanLogs" = "Get Ban Logs"
 
 [tgbot.answers]
 "successfulOperation" = "✅ Successful!"
@@ -548,5 +559,4 @@
 "removedTGUserSuccess" = "✅ {{ .Email }} : Telegram User removed successfully."
 "enableSuccess" = "✅ {{ .Email }} : Enabled successfully."
 "disableSuccess" = "✅ {{ .Email }} : Disabled successfully."
-"askToAddUserId" = "Your configuration is not found!\r\nPlease ask your Admin to use your telegram user id in your configuration(s).\r\n\r\nYour user id: <b>{{ .TgUserID }}</b>"
-"askToAddUserName" = "Your configuration is not found!\r\nPlease ask your Admin to use your telegram username or user id in your configuration(s).\r\n\r\nYour username: <b>@{{ .TgUserName }}</b>\r\n\r\nYour user id: <b>{{ .TgUserID }}</b>"
+"askToAddUserId" = "Your configuration is not found!\r\nPlease ask your Admin to use your telegram user id in your configuration(s).\r\n\r\nYour user id: <code>{{ .TgUserID }}</code>"

+ 24 - 14
web/translation/translate.es_ES.toml

@@ -173,7 +173,7 @@
 "setDefaultCert" = "Establecer certificado desde el panel"
 "xtlsDesc" = "La versión del núcleo de Xray debe ser 1.7.5"
 "realityDesc" = "La versión del núcleo de Xray debe ser 1.8.0 o superior."
-"telegramDesc" = "Utiliza el ID de Telegram sin @ o los IDs de chat (puedes obtenerlo aquí @userinfobot o usando el comando '/id' en el bot)."
+"telegramDesc" = "Utiliza únicamente IDs de chat (puedes obtenerlo aquí @userinfobot o usando el comando '/id' en el bot)."
 "subscriptionDesc" = "Puedes encontrar tu enlace de suscripción en Detalles, también puedes usar el mismo nombre para varias configuraciones."
 "info" = "Info"
 "same" = "misma"
@@ -439,6 +439,7 @@
 "noIpRecord" = "❗ ¡Sin Registro de IP!"
 "noInbounds" = "❗ ¡No se encontraron entradas!"
 "unlimited" = "♾ Ilimitado"
+"add" = "Agregar"
 "month" = "Mes"
 "months" = "Meses"
 "day" = "Día"
@@ -447,6 +448,8 @@
 "unknown" = "Desconocido"
 "inbounds" = "Entradas"
 "clients" = "Clientes"
+"offline" = "🔴 Sin conexión"
+"online" = "🟢 En línea"
 
 [tgbot.commands]
 "unknown" = "❗ Comando desconocido"
@@ -457,8 +460,8 @@
 "status" = "✅ ¡El bot está bien!"
 "usage" = "❗ ¡Por favor proporciona un texto para buscar!"
 "getID" = "🆔 Tu ID: <code>{{ .ID }}</code>"
-"helpAdminCommands" = "Buscar un correo electrónico de cliente:\r\n<code>/usage [Email]</code>\r\n \r\nBuscar entradas (con estadísticas de cliente):\r\n<code>/inbound [Nota]</code>"
-"helpClientCommands" = "Para buscar estadísticas, simplemente usa el siguiente comando:\r\n \r\n<code>/usage [UUID|Contraseña]</code>\r\n \r\nUsa UUID para vmess/vless y Contraseña para Trojan."
+"helpAdminCommands" = "Buscar un correo electrónico de cliente:\r\n<code>/usage [Email]</code>\r\n\r\nBuscar entradas (con estadísticas de cliente):\r\n<code>/inbound [Nota]</code>"
+"helpClientCommands" = "Para buscar estadísticas, simplemente usa el siguiente comando:\r\n\r\n<code>/usage [UUID|Contraseña]</code>"
 
 [tgbot.messages]
 "cpuThreshold" = "🔴 El uso de CPU {{ .Percent }}% es mayor que el umbral {{ .Threshold }}%"
@@ -473,7 +476,7 @@
 "ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
 "ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
 "ip" = "🌐 IP: {{ .IP }}\r\n"
-"ips" = "🔢 IPs: \r\n{{ .IPs }}\r\n"
+"ips" = "🔢 IPs:\r\n{{ .IPs }}\r\n"
 "serverUpTime" = "⏳ Tiempo de actividad del servidor: {{ .UpTime }} {{ .Unit }}\r\n"
 "serverLoad" = "📈 Carga del servidor: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
 "serverMemory" = "📋 Memoria del servidor: {{ .Current }}/{{ .Total }}\r\n"
@@ -487,19 +490,23 @@
 "port" = "🔌 Puerto: {{ .Port }}\r\n"
 "expire" = "📅 Fecha de Vencimiento: {{ .Time }}\r\n"
 "expireIn" = "📅 Vence en: {{ .Time }}\r\n"
-"active" = "💡 Activo: ✅ Sí\r\n"
-"inactive" = "💡 Activo: ❌ No\r\n"
+"active" = "💡 Activo: {{ .Enable }}\r\n"
+"enabled" = "🚨 Habilitado: {{ .Enable }}\r\n"
+"online" = "🌐 Estado de conexión: {{ .Status }}\r\n"
 "email" = "📧 Email: {{ .Email }}\r\n"
 "upload" = "🔼 Subida: ↑{{ .Upload }}\r\n"
 "download" = "🔽 Bajada: ↓{{ .Download }}\r\n"
 "total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
 "TGUser" = "👤 Usuario de Telegram: {{ .TelegramID }}\r\n"
-"exhaustedMsg" = "🚨 Agotado {{ .Type }}: \r\n"
-"exhaustedCount" = "🚨 Cantidad de Agotados {{ .Type }}: \r\n"
+"exhaustedMsg" = "🚨 Agotado {{ .Type }}:\r\n"
+"exhaustedCount" = "🚨 Cantidad de Agotados {{ .Type }}:\r\n"
+"onlinesCount" = "🌐 Clientes en línea: {{ .Count }}\r\n"
 "disabled" = "🛑 Desactivado: {{ .Disabled }}\r\n"
-"depleteSoon" = "🔜 Se agotará pronto: {{ .Deplete }}\r\n \r\n"
+"depleteSoon" = "🔜 Se agotará pronto: {{ .Deplete }}\r\n\r\n"
 "backupTime" = "🗄 Hora de la Copia de Seguridad: {{ .Time }}\r\n"
-"refreshedOn" = "\r\n📋🔄 Actualizado en: {{ .Time }}\r\n \r\n"
+"refreshedOn" = "\r\n📋🔄 Actualizado en: {{ .Time }}\r\n\r\n"
+"yes" = "✅ Sí"
+"no" = "❌ No"
 
 [tgbot.buttons]
 "closeKeyboard" = "❌ Cerrar Teclado"
@@ -509,11 +516,13 @@
 "confirmResetTraffic" = "✅ ¿Confirmar Reinicio de Tráfico?"
 "confirmClearIps" = "✅ ¿Confirmar Limpiar IPs?"
 "confirmRemoveTGUser" = "✅ ¿Confirmar Eliminar Usuario de Telegram?"
+"confirmToggle" = " ✅ ¿Confirmar habilitar/deshabilitar usuario?"
 "dbBackup" = "Obtener Copia de Seguridad de BD"
 "serverUsage" = "Uso del Servidor"
 "getInbounds" = "Obtener Entradas"
 "depleteSoon" = "Pronto se Agotará"
 "clientUsage" = "Obtener Uso"
+"onlines" = "Clientes en línea"
 "commands" = "Comandos"
 "refresh" = "🔄 Actualizar"
 "clearIPs" = "❌ Limpiar IPs"
@@ -521,14 +530,16 @@
 "selectTGUser" = "👤 Seleccionar Usuario de Telegram"
 "selectOneTGUser" = "👤 Selecciona un usuario de telegram:"
 "resetTraffic" = "📈 Reiniciar Tráfico"
-"resetExpire" = "📅 Reiniciar Días de Vencimiento"
+"resetExpire" = "📅 Cambiar fecha de Vencimiento"
 "ipLog" = "🔢 Registro de IP"
 "ipLimit" = "🔢 Límite de IP"
 "setTGUser" = "👤 Establecer Usuario de Telegram"
 "toggle" = "🔘 Habilitar / Deshabilitar"
 "custom" = "🔢 Costumbre"
-"confirmNumber" = "✅ Confirmar : {{ .Num }}"
+"confirmNumber" = "✅ Confirmar: {{ .Num }}"
+"confirmNumberAdd" = "✅ Confirmar agregando: {{ .Num }}"
 "limitTraffic" = "🚧 Límite de tráfico"
+"getBanLogs" = "Registros de prohibición"
 
 [tgbot.answers]
 "successfulOperation" = "✅ ¡Exitosa!"
@@ -548,5 +559,4 @@
 "removedTGUserSuccess" = "✅ {{ .Email }} : Usuario de Telegram eliminado exitosamente."
 "enableSuccess" = "✅ {{ .Email }} : Habilitado exitosamente."
 "disableSuccess" = "✅ {{ .Email }} : Deshabilitado exitosamente."
-"askToAddUserId" = "¡No se encuentra su configuración!\r\nPor favor, pídale a su administrador que use su ID de usuario de Telegram en su(s) configuración(es).\r\n\r\nSu ID de usuario: <b>{{ .TgUserID }}</b>"
-"askToAddUserName" = "¡No se encuentra su configuración!\r\nPor favor, pídale a su administrador que use su nombre de usuario o ID de usuario de Telegram en su(s) configuración(es).\r\n\r\nSu nombre de usuario: <b>@{{ .TgUserName }}</b>\r\n\r\nSu ID de usuario: <b>{{ .TgUserID }}</b>"
+"askToAddUserId" = "¡No se encuentra su configuración!\r\nPor favor, pídale a su administrador que use su ID de usuario de Telegram en su(s) configuración(es).\r\n\r\nSu ID de usuario: <code>{{ .TgUserID }}</code>"

+ 21 - 11
web/translation/translate.fa_IR.toml

@@ -173,7 +173,7 @@
 "setDefaultCert" = "استفاده از گواهی پنل"
 "xtlsDesc" = "هسته Xray باید 1.7.5 باشد"
 "realityDesc" = "هسته Xray باید 1.8.0 و بالاتر باشد"
-"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)"
+"telegramDesc" = "فقط از شناسه چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)"
 "subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
 "info" = "اطلاعات"
 "same" = "همسان"
@@ -439,6 +439,7 @@
 "noIpRecord" = "❗ رکورد IP یافت نشد!"
 "noInbounds" = "❗ هیچ ورودی یافت نشد!"
 "unlimited" = "♾ نامحدود"
+"add" = "اضافه کردن"
 "month" = "ماه"
 "months" = "ماه‌ها"
 "day" = "روز"
@@ -447,6 +448,8 @@
 "unknown" = "نامشخص"
 "inbounds" = "ورودی‌ها"
 "clients" = "کلاینت‌ها"
+"offline" = "🔴 آفلاین"
+"online" = "🟢 برخط"
 
 [tgbot.commands]
 "unknown" = "❗ دستور ناشناخته"
@@ -457,8 +460,8 @@
 "status" = "✅ ربات در حالت عادی است!"
 "usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!"
 "getID" = "🆔 شناسه شما: <code>{{ .ID }}</code>"
-"helpAdminCommands" = "برای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n \r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n<code>/inbound [توضیح]</code>"
-"helpClientCommands" = "برای جستجوی آمار، فقط از دستور زیر استفاده کنید:\r\n \r\n<code>/usage [UUID|رمز عبور]</code>\r\n \r\nاز UUID برای vmess/vless و از رمز عبور برای Trojan استفاده کنید."
+"helpAdminCommands" = "برای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n\r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n<code>/inbound [توضیح]</code>"
+"helpClientCommands" = "برای جستجوی آمار، فقط از دستور زیر استفاده کنید:\r\n\r\n<code>/usage [Email]</code>"
 
 [tgbot.messages]
 "cpuThreshold" = "🔴 میزان استفاده از CPU {{ .Percent }}% بیشتر از آستانه {{ .Threshold }}% است."
@@ -473,7 +476,7 @@
 "ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
 "ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
 "ip" = "🌐 آدرس IP: {{ .IP }}\r\n"
-"ips" = "🔢 آدرس‌های IP: \r\n{{ .IPs }}\r\n"
+"ips" = "🔢 آدرس‌های IP:\r\n{{ .IPs }}\r\n"
 "serverUpTime" = "⏳ زمان کارکرد سرور: {{ .UpTime }} {{ .Unit }}\r\n"
 "serverLoad" = "📈 بار سرور: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
 "serverMemory" = "📋 حافظه سرور: {{ .Current }}/{{ .Total }}\r\n"
@@ -487,8 +490,9 @@
 "port" = "🔌 پورت: {{ .Port }}\r\n"
 "expire" = "📅 تاریخ انقضا: {{ .Time }}\r\n"
 "expireIn" = "📅 باقیمانده از انقضا: {{ .Time }}\r\n"
-"active" = "💡 فعال: ✅\r\n"
-"inactive" = "💡 فعال: ❌\r\n"
+"active" = "💡 فعال: {{ .Enable }}\r\n"
+"enabled" = "🚨 مشمول: {{ .Enable }}\r\n"
+"online" = "🌐 وضعیت اتصال: {{ .Status }}\r\n"
 "email" = "📧 ایمیل: {{ .Email }}\r\n"
 "upload" = "🔼 آپلود↑: {{ .Upload }}\r\n"
 "download" = "🔽 دانلود↓: {{ .Download }}\r\n"
@@ -496,10 +500,13 @@
 "TGUser" = "👤 کاربر تلگرام: {{ .TelegramID }}\r\n"
 "exhaustedMsg" = "🚨 {{ .Type }} به اتمام رسیده است:\r\n"
 "exhaustedCount" = "🚨 تعداد {{ .Type }} به اتمام رسیده:\r\n"
+"onlinesCount" = "🌐 مشتریان آنلاین: {{ .Count }}\r\n"
 "disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n"
-"depleteSoon" = "🔜 به زودی به پایان خواهد رسید: {{ .Deplete }}\r\n \r\n"
+"depleteSoon" = "🔜 به زودی به پایان خواهد رسید: {{ .Deplete }}\r\n\r\n"
 "backupTime" = "🗄 زمان پشتیبان‌گیری: {{ .Time }}\r\n"
-"refreshedOn" = "\r\n📋🔄 تازه‌سازی شده در: {{ .Time }}\r\n \r\n"
+"refreshedOn" = "\r\n📋🔄 تازه‌سازی شده در: {{ .Time }}\r\n\r\n"
+"yes" = "✅ بله"
+"no" = "❌ نه"
 
 [tgbot.buttons]
 "closeKeyboard" = "❌ بستن کیبورد"
@@ -509,11 +516,13 @@
 "confirmResetTraffic" = "✅ تأیید تنظیم مجدد ترافیک؟"
 "confirmClearIps" = "✅ تأیید پاک‌سازی آدرس‌های IP؟"
 "confirmRemoveTGUser" = "✅ تأیید حذف کاربر تلگرام؟"
+"confirmToggle" = "✅ تایید فعال/غیرفعال کردن کاربر؟"
 "dbBackup" = "دریافت پشتیبان پایگاه داده"
 "serverUsage" = "استفاده از سرور"
 "getInbounds" = "دریافت ورودی‌ها"
 "depleteSoon" = "به زودی به پایان خواهد رسید"
 "clientUsage" = "دریافت آمار کاربر"
+"onlines" = "مشتریان آنلاین"
 "commands" = "دستورات"
 "refresh" = "🔄 تازه‌سازی"
 "clearIPs" = "❌ پاک‌سازی آدرس‌ها"
@@ -527,8 +536,10 @@
 "setTGUser" = "👤 تنظیم کاربر تلگرام"
 "toggle" = "🔘 فعال / غیرفعال"
 "custom" = "🔢 سفارشی"
-"confirmNumber" = "✅ تایید : {{ .Num }}"
+"confirmNumber" = "✅ تایید: {{ .Num }}"
+"confirmNumberAdd" = "✅ تایید اضافه کردن: {{ .Num }}"
 "limitTraffic" = "🚧 محدودیت ترافیک"
+"getBanLogs" = "گزارش های بلوک را دریافت کنید"
 
 [tgbot.answers]
 "successfulOperation" = "✅ انجام شد!"
@@ -548,5 +559,4 @@
 "removedTGUserSuccess" = "✅ {{ .Email }} : کاربر تلگرام با موفقیت حذف شد."
 "enableSuccess" = "✅ {{ .Email }} : با موفقیت فعال شد."
 "disableSuccess" = "✅ {{ .Email }} : با موفقیت غیرفعال شد."
-"askToAddUserId" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nشناسه کاربری شما: <b>{{ .TgUserID }}</b>"
-"askToAddUserName" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که نام کاربری یا شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nنام کاربری شما: <b>@{{ .TgUserName }}</b>\r\n\r\nشناسه کاربری شما: <b>{{ .TgUserID }}</b>"
+"askToAddUserId" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nشناسه کاربری شما: <code>{{ .TgUserID }}</code>"

+ 22 - 12
web/translation/translate.ru_RU.toml

@@ -173,7 +173,7 @@
 "setDefaultCert" = "Установить сертификат с панели"
 "xtlsDesc" = "Версия Xray должна быть не ниже 1.7.5"
 "realityDesc" = "Версия Xray должна быть не ниже 1.8.0"
-"telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)"
+"telegramDesc" = "Используйте только ID чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)"
 "subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее', также вы можете использовать одно и то же имя для нескольких конфигураций"
 "info" = "Информация"
 "same" = "Тот же"
@@ -439,6 +439,7 @@
 "noIpRecord" = "❗ Нет записей об IP-адресе!"
 "noInbounds" = "❗ Входящих соединений не найдено!"
 "unlimited" = "♾ Неограниченно"
+"add" = "Добавить"
 "month" = "Месяц"
 "months" = "Месяцев"
 "day" = "День"
@@ -447,6 +448,8 @@
 "unknown" = "Неизвестно"
 "inbounds" = "Входящие"
 "clients" = "Клиенты"
+"offline" = "🔴 Офлайн"
+"online" = "🟢 Онлайн"
 
 [tgbot.commands]
 "unknown" = "❗ Неизвестная команда"
@@ -457,8 +460,8 @@
 "status" = "✅ Бот работает нормально!"
 "usage" = "❗ Пожалуйста, укажите текст для поиска!"
 "getID" = "🆔 Ваш ID: <code>{{ .ID }}</code>"
-"helpAdminCommands" = "Поиск по электронной почте клиента:\r\n<code>/usage [Email]</code>\r\n \r\nПоиск входящих соединений (со статистикой клиента):\r\n<code>/inbound [Remark]</code>"
-"helpClientCommands" = "Для получения статистики используйте следующую команду:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nИспользуйте UUID для vmess/vless и пароль для Trojan."
+"helpAdminCommands" = "Поиск по электронной почте клиента:\r\n<code>/usage [Email]</code>\r\n\r\nПоиск входящих соединений (со статистикой клиента):\r\n<code>/inbound [Remark]</code>"
+"helpClientCommands" = "Для получения статистики используйте следующую команду:\r\n\r\n<code>/usage [Email]</code>"
 
 [tgbot.messages]
 "cpuThreshold" = "🔴 Загрузка процессора составляет {{ .Percent }}%, что превышает пороговое значение {{ .Threshold }}%"
@@ -473,7 +476,7 @@
 "ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
 "ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
 "ip" = "🌐 IP: {{ .IP }}\r\n"
-"ips" = "🔢 IP-адреса: \r\n{{ .IPs }}\r\n"
+"ips" = "🔢 IP-адреса:\r\n{{ .IPs }}\r\n"
 "serverUpTime" = "⏳ Время работы сервера: {{ .UpTime }} {{ .Unit }}\r\n"
 "serverLoad" = "📈 Загрузка сервера: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
 "serverMemory" = "📋 Память сервера: {{ .Current }}/{{ .Total }}\r\n"
@@ -488,7 +491,8 @@
 "expire" = "📅 Дата окончания: {{ .Time }}\r\n"
 "expireIn" = "📅 Окончание через: {{ .Time }}\r\n"
 "active" = "💡 Активен: ✅ Да\r\n"
-"inactive" = "💡 Активен: ❌ Нет\r\n"
+"enabled" = "🚨 Включен: {{ .Enable }}\r\n"
+"online" = "🌐 Статус соединения: {{ .Status }}\r\n"
 "email" = "📧 Email: {{ .Email }}\r\n"
 "upload" = "🔼 Исходящий трафик: ↑{{ .Upload }}\r\n"
 "download" = "🔽 Входящий трафик: ↓{{ .Download }}\r\n"
@@ -496,10 +500,13 @@
 "TGUser" = "👤 Пользователь Telegram: {{ .TelegramID }}\r\n"
 "exhaustedMsg" = "🚨 Исчерпаны {{ .Type }}:\r\n"
 "exhaustedCount" = "🚨 Количество исчерпанных {{ .Type }}:\r\n"
+"onlinesCount" = "🌐 Клиентов онлайн: {{ .Count }}\r\n"
 "disabled" = "🛑 Отключено: {{ .Disabled }}\r\n"
-"depleteSoon" = "🔜 Скоро исчерпание: {{ .Deplete }}\r\n \r\n"
+"depleteSoon" = "🔜 Скоро исчерпание: {{ .Deplete }}\r\n\r\n"
 "backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n"
-"refreshedOn" = "\r\n📋🔄 Обновлено: {{ .Time }}\r\n \r\n"
+"refreshedOn" = "\r\n📋🔄 Обновлено: {{ .Time }}\r\n\r\n"
+"yes" = "✅ Да"
+"no" = "❌ Нет"
 
 [tgbot.buttons]
 "closeKeyboard" = "❌ Закрыть клавиатуру"
@@ -509,11 +516,13 @@
 "confirmResetTraffic" = "✅ Подтвердить сброс трафика?"
 "confirmClearIps" = "✅ Подтвердить очистку IP?"
 "confirmRemoveTGUser" = "✅ Подтвердить удаление пользователя Telegram?"
+"confirmToggle" = "✅ Подтвердить вкл/выкл пользователя?"
 "dbBackup" = "Получить резервную копию DB"
 "serverUsage" = "Использование сервера"
 "getInbounds" = "Получить входящие потоки"
 "depleteSoon" = "Скоро исчерпание"
 "clientUsage" = "Получить использование"
+"onlines" = "Онлайн-клиенты"
 "commands" = "Команды"
 "refresh" = "🔄 Обновить"
 "clearIPs" = "❌ Очистить IP"
@@ -521,14 +530,16 @@
 "selectTGUser" = "👤 Выбрать пользователя Telegram"
 "selectOneTGUser" = "👤 Выберите пользователя Telegram:"
 "resetTraffic" = "📈 Сбросить трафик"
-"resetExpire" = "📅 Сбросить дату окончания"
+"resetExpire" = "📅 Изменить дату окончания"
 "ipLog" = "🔢 Лог IP"
 "ipLimit" = "🔢 Лимит IP"
 "setTGUser" = "👤 Установить пользователя Telegram"
 "toggle" = "🔘 Вкл./Выкл."
-"custom" = "🔢 Обычай"
-"confirmNumber" = "✅ Подтвердить : {{ .Num }}"
+"custom" = "🔢 Свой"
+"confirmNumber" = "✅ Подтвердить: {{ .Num }}"
+"confirmNumberAdd" = "✅ Подтвердить добавление: {{ .Num }}"
 "limitTraffic" = "🚧 Лимит трафика"
+"getBanLogs" = "Логи блокировок"
 
 [tgbot.answers]
 "successfulOperation" = "✅ Успешный!"
@@ -548,5 +559,4 @@
 "removedTGUserSuccess" = "✅ {{ .Email }}: Пользователь Telegram успешно удален."
 "enableSuccess" = "✅ {{ .Email }}: Включено успешно."
 "disableSuccess" = "✅ {{ .Email }}: Отключено успешно."
-"askToAddUserId" = "Ваша конфигурация не найдена!\r\nПожалуйста, попросите администратора использовать ваш идентификатор пользователя Telegram в ваших конфигурациях.\r\n\r\nВаш идентификатор пользователя: <b>{{ .TgUserID }}</b>"
-"askToAddUserName" = "Ваша конфигурация не найдена!\r\nПожалуйста, попросите администратора использовать ваше имя пользователя или идентификатор пользователя Telegram в ваших конфигурациях.\r\n\r\nВаше имя пользователя: <b>@{{ .TgUserName }}</b>\r\n\r\nВаш идентификатор пользователя: <b>{{ .TgUserID }}</b>"
+"askToAddUserId" = "Ваша конфигурация не найдена!\r\nПожалуйста, попросите администратора использовать ваш идентификатор пользователя Telegram в ваших конфигурациях.\r\n\r\nВаш идентификатор пользователя: <code>{{ .TgUserID }}</code>"

+ 22 - 12
web/translation/translate.vi_VN.toml

@@ -173,7 +173,7 @@
 "setDefaultCert" = "Đặt chứng chỉ từ bảng điều khiển"
 "xtlsDesc" = "Xray core cần phiên bản 1.7.5"
 "realityDesc" = "Xray core cần phiên bản 1.8.0 hoặc cao hơn."
-"telegramDesc" = "Sử dụng Telegram ID mà không cần ký hiệu @ hoặc chat IDs (bạn có thể nhận được nó ở đây @userinfobot hoặc sử dụng lệnh '/id' trong bot)"
+"telegramDesc" = "Chỉ sử dụng ID trò chuyện (bạn có thể nhận được nó ở đây @userinfobot hoặc sử dụng lệnh '/id' trong bot)"
 "subscriptionDesc" = "Bạn có thể tìm liên kết đăng ký của mình trong Chi tiết, cũng như bạn có thể sử dụng cùng tên cho nhiều cấu hình khác nhau"
 "info" = "Thông tin"
 "same" = "Giống nhau"
@@ -439,6 +439,7 @@
 "noIpRecord" = "❗ Không có bản ghi IP!"
 "noInbounds" = "❗ Không tìm thấy inbound!"
 "unlimited" = "♾ Không giới hạn"
+"add" = "Thêm"
 "month" = "Tháng"
 "months" = "Tháng"
 "day" = "Ngày"
@@ -447,6 +448,8 @@
 "unknown" = "Không rõ"
 "inbounds" = "Vào"
 "clients" = "Các khách hàng"
+"offline" = "🔴 Ngoại tuyến"
+"online" = "🟢 Trực tuyến"
 
 [tgbot.commands]
 "unknown" = "❗ Lệnh không rõ"
@@ -457,8 +460,8 @@
 "status" = "✅ Bot hoạt động bình thường!"
 "usage" = "❗ Vui lòng cung cấp văn bản để tìm kiếm!"
 "getID" = "🆔 ID của bạn: <code>{{ .ID }}</code>"
-"helpAdminCommands" = "Tìm kiếm email của khách hàng:\r\n<code>/usage [Email]</code>\r\n \r\nTìm kiếm inbounds (với thống kê của khách hàng):\r\n<code>/inbound [Ghi chú]</code>"
-"helpClientCommands" = "Để tìm kiếm thống kê, hãy sử dụng lệnh sau:\r\n \r\n<code>/usage [UUID|Mật khẩu]</code>\r\n \r\nSử dụng UUID cho vmess/vless và Mật khẩu cho Trojan."
+"helpAdminCommands" = "Tìm kiếm email của khách hàng:\r\n<code>/usage [Email]</code>\r\n\r\nTìm kiếm inbounds (với thống kê của khách hàng):\r\n<code>/inbound [Ghi chú]</code>"
+"helpClientCommands" = "Để tìm kiếm thống kê, hãy sử dụng lệnh sau:\r\n\r\n<code>/usage [Email]</code>"
 
 [tgbot.messages]
 "cpuThreshold" = "🔴 Sử dụng CPU {{ .Percent }}% vượt quá ngưỡng {{ .Threshold }}%"
@@ -473,7 +476,7 @@
 "ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
 "ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
 "ip" = "🌐 IP: {{ .IP }}\r\n"
-"ips" = "🔢 Các IP: \r\n{{ .IPs }}\r\n"
+"ips" = "🔢 Các IP:\r\n{{ .IPs }}\r\n"
 "serverUpTime" = "⏳ Thời gian hoạt động của máy chủ: {{ .UpTime }} {{ .Unit }}\r\n"
 "serverLoad" = "📈 Tải máy chủ: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
 "serverMemory" = "📋 Bộ nhớ máy chủ: {{ .Current }}/{{ .Total }}\r\n"
@@ -487,8 +490,9 @@
 "port" = "🔌 Cổng: {{ .Port }}\r\n"
 "expire" = "📅 Ngày hết hạn: {{ .Time }}\r\n"
 "expireIn" = "📅 Hết hạn sau: {{ .Time }}\r\n"
-"active" = "💡 Hoạt động: ✅ Có\r\n"
-"inactive" = "💡 Hoạt động: ❌ Không\r\n"
+"active" = "💡 Đang hoạt động: {{ .Enable }}\r\n"
+"enabled" = "🚨 Đã bật: {{ .Enable }}\r\n"
+"online" = "🌐 Trạng thái kết nối: {{ .Status }}\r\n"
 "email" = "📧 Email: {{ .Email }}\r\n"
 "upload" = "🔼 Tải lên: ↑{{ .Upload }}\r\n"
 "download" = "🔽 Tải xuống: ↓{{ .Download }}\r\n"
@@ -496,10 +500,13 @@
 "TGUser" = "👤 Người dùng Telegram: {{ .TelegramID }}\r\n"
 "exhaustedMsg" = "🚨 Sự cạn kiệt {{ .Type }}:\r\n"
 "exhaustedCount" = "🚨 Số lần cạn kiệt {{ .Type }}:\r\n"
+"onlinesCount" = "🌐 Khách hàng trực tuyến: {{ .Count }}\r\n"
 "disabled" = "🛑 Vô hiệu hóa: {{ .Disabled }}\r\n"
-"depleteSoon" = "🔜 Sắp cạn kiệt: {{ .Deplete }}\r\n \r\n"
+"depleteSoon" = "🔜 Sắp cạn kiệt: {{ .Deplete }}\r\n\r\n"
 "backupTime" = "🗄 Thời gian sao lưu: {{ .Time }}\r\n"
-"refreshedOn" = "\r\n📋🔄 Đã cập nhật lần cuối vào: {{ .Time }}\r\n \r\n"
+"refreshedOn" = "\r\n📋🔄 Đã cập nhật lần cuối vào: {{ .Time }}\r\n\r\n"
+"yes" = "✅ Có"
+"no" = "❌ Không"
 
 [tgbot.buttons]
 "closeKeyboard" = "❌ Đóng Bàn Phím"
@@ -509,11 +516,13 @@
 "confirmResetTraffic" = "✅ Xác Nhận Đặt Lại Lưu Lượng?"
 "confirmClearIps" = "✅ Xác Nhận Xóa Các IP?"
 "confirmRemoveTGUser" = "✅ Xác Nhận Xóa Người Dùng Telegram?"
+"confirmToggle" = "✅ Xác nhận Bật/Tắt người dùng?"
 "dbBackup" = "Tải bản sao lưu cơ sở dữ liệu"
 "serverUsage" = "Sử Dụng Máy Chủ"
 "getInbounds" = "Lấy cổng vào"
 "depleteSoon" = "Depleted Soon"
 "clientUsage" = "Lấy Sử Dụng"
+"onlines" = "Khách hàng trực tuyến"
 "commands" = "Lệnh"
 "refresh" = "🔄 Cập Nhật"
 "clearIPs" = "❌ Xóa IP"
@@ -521,14 +530,16 @@
 "selectTGUser" = "👤 Chọn Người Dùng Telegram"
 "selectOneTGUser" = "👤 Chọn một người dùng telegram:"
 "resetTraffic" = "📈 Đặt Lại Lưu Lượng"
-"resetExpire" = "📅 Đặt Lại Ngày Hết Hạn"
+"resetExpire" = "📅 Thay đổi ngày hết hạn"
 "ipLog" = "🔢 Nhật ký địa chỉ IP"
 "ipLimit" = "🔢 Giới Hạn địa chỉ IP"
 "setTGUser" = "👤 Đặt Người Dùng Telegram"
 "toggle" = "🔘 Bật / Tắt"
 "custom" = "🔢 Tùy chỉnh"
-"confirmNumber" = "✅ Xác nhận : {{ .Num }}"
+"confirmNumber" = "✅ Xác nhận: {{ .Num }}"
+"confirmNumberAdd" = "✅ Xác nhận thêm: {{ .Num }}"
 "limitTraffic" = "🚧 Giới hạn lưu lượng"
+"getBanLogs" = "Cấm nhật ký"
 
 [tgbot.answers]
 "successfulOperation" = "✅ Thành công!"
@@ -548,5 +559,4 @@
 "removedTGUserSuccess" = "✅ {{ .Email }} : Người Dùng Telegram Đã Được Xóa Thành Công."
 "enableSuccess" = "✅ {{ .Email }} : Đã Bật Thành Công."
 "disableSuccess" = "✅ {{ .Email }} : Đã Tắt Thành Công."
-"askToAddUserId" = "Cấu hình của bạn không được tìm thấy!\r\nVui lòng yêu cầu Quản trị viên sử dụng ID người dùng telegram của bạn trong cấu hình của bạn.\r\n\r\nID người dùng của bạn: <b>{{ .TgUserID }}</b>"
-"askToAddUserName" = "Cấu hình của bạn không được tìm thấy!\r\nVui lòng yêu cầu Quản trị viên sử dụng tên người dùng hoặc ID người dùng telegram của bạn trong cấu hình của bạn.\r\n\r\nTên người dùng của bạn: <b>@{{ .TgUserName }}</b>\r\n\r\nID người dùng của bạn: <b>{{ .TgUserID }}</b>"
+"askToAddUserId" = "Cấu hình của bạn không được tìm thấy!\r\nVui lòng yêu cầu Quản trị viên sử dụng ID người dùng telegram của bạn trong cấu hình của bạn.\r\n\r\nID người dùng của bạn: <code>{{ .TgUserID }}</code>"

+ 20 - 10
web/translation/translate.zh_Hans.toml

@@ -173,7 +173,7 @@
 "setDefaultCert" = "从面板设置证书"
 "xtlsDesc" = "Xray核心需要1.7.5"
 "realityDesc" = "Xray核心需要1.8.0及以上版本"
-"telegramDesc" = "使用 Telegram ID,不包含 @ 符号或聊天 ID(可以在 @userinfobot 处获取,或在机器人中使用'/id'命令)"
+"telegramDesc" = "使用聊天 ID(可以在 @userinfobot 处获取,或在机器人中使用'/id'命令)"
 "subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
 "info" = "信息"
 "same" = "相同"
@@ -439,6 +439,7 @@
 "noIpRecord" = "❗ 没有IP记录!"
 "noInbounds" = "❗ 没有找到入站连接!"
 "unlimited" = "♾ 无限制"
+"add" = "添加"
 "month" = "月"
 "months" = "月"
 "day" = "天"
@@ -447,6 +448,8 @@
 "unknown" = "未知"
 "inbounds" = "入站连接"
 "clients" = "客户端"
+"offline" = "🔴 离线"
+"online" = "🟢 在线的"
 
 [tgbot.commands]
 "unknown" = "❗ 未知命令"
@@ -457,8 +460,8 @@
 "status" = "✅ 机器人正常运行!"
 "usage" = "❗ 请输入要搜索的文本!"
 "getID" = "🆔 您的ID为:<code>{{ .ID }}</code>"
-"helpAdminCommands" = "搜索客户端邮箱:\r\n<code>/usage [Email]</code>\r\n \r\n搜索入站连接(包含客户端统计信息):\r\n<code>/inbound [Remark]</code>"
-"helpClientCommands" = "要搜索统计信息,请使用以下命令:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\n对于vmess/vless,请使用UUID;对于Trojan,请使用密码。"
+"helpAdminCommands" = "搜索客户端邮箱:\r\n<code>/usage [Email]</code>\r\n\r\n搜索入站连接(包含客户端统计信息):\r\n<code>/inbound [Remark]</code>"
+"helpClientCommands" = "要搜索统计信息,请使用以下命令:\r\n\r\n<code>/usage [Email]</code>"
 
 [tgbot.messages]
 "cpuThreshold" = "🔴 CPU 使用率为 {{ .Percent }}%,超过阈值 {{ .Threshold }}%"
@@ -488,7 +491,8 @@
 "expire" = "📅 过期日期:{{ .Time }}\r\n"
 "expireIn" = "📅 剩余时间:{{ .Time }}\r\n"
 "active" = "💡 激活:✅\r\n"
-"inactive" = "💡 激活: ❌\r\n"
+"enabled" = "🚨 已启用:{{ .Enable }}\r\n"
+"online" = "🌐 连接状态:{{ .Status }}\r\n"
 "email" = "📧 邮箱:{{ .Email }}\r\n"
 "upload" = "🔼 上传↑:{{ .Upload }}\r\n"
 "download" = "🔽 下载↓:{{ .Download }}\r\n"
@@ -496,10 +500,13 @@
 "TGUser" = "👤 电报用户:{{ .TelegramID }}\r\n"
 "exhaustedMsg" = "🚨 耗尽的{{ .Type }}:\r\n"
 "exhaustedCount" = "🚨 耗尽的{{ .Type }}数量:\r\n"
+"onlinesCount" = "🌐 在线客户:{{ .Count }}\r\n"
 "disabled" = "🛑 禁用:{{ .Disabled }}\r\n"
-"depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n \r\n"
+"depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n\r\n"
 "backupTime" = "🗄 备份时间:{{ .Time }}\r\n"
-"refreshedOn" = "\r\n📋🔄 刷新时间:{{ .Time }}\r\n \r\n"
+"refreshedOn" = "\r\n📋🔄 刷新时间:{{ .Time }}\r\n\r\n"
+"yes" = "✅ 是的"
+"no" = "❌ 没有"
 
 [tgbot.buttons]
 "closeKeyboard" = "❌ 关闭键盘"
@@ -509,11 +516,13 @@
 "confirmResetTraffic" = "✅ 确认重置流量?"
 "confirmClearIps" = "✅ 确认清除 IP?"
 "confirmRemoveTGUser" = "✅ 确认移除 Telegram 用户?"
+"confirmToggle" = "✅ 确认启用/禁用用户?"
 "dbBackup" = "获取数据库备份"
 "serverUsage" = "服务器使用情况"
 "getInbounds" = "获取入站信息"
 "depleteSoon" = "即将耗尽"
 "clientUsage" = "获取使用情况"
+"onlines" = "在线客户"
 "commands" = "命令"
 "refresh" = "🔄 刷新"
 "clearIPs" = "❌ 清除 IP"
@@ -521,14 +530,16 @@
 "selectTGUser" = "👤 选择 Telegram 用户"
 "selectOneTGUser" = "👤 选择一个 Telegram 用户:"
 "resetTraffic" = "📈 重置流量"
-"resetExpire" = "📅 重置过期天数"
+"resetExpire" = "📅 更改到期日期"
 "ipLog" = "🔢 IP 日志"
 "ipLimit" = "🔢 IP 限制"
 "setTGUser" = "👤 设置 Telegram 用户"
 "toggle" = "🔘 启用/禁用"
 "custom" = "🔢 风俗"
-"confirmNumber" = "✅ 确认 : {{ .Num }}"
+"confirmNumber" = "✅ 确认: {{ .Num }}"
+"confirmNumberAdd" = "✅ 确认添加:{{ .Num }}"
 "limitTraffic" = "🚧 交通限制"
+"getBanLogs" = "禁止日志"
 
 [tgbot.answers]
 "successfulOperation" = "✅ 成功的!"
@@ -548,5 +559,4 @@
 "removedTGUserSuccess" = "✅ {{ .Email }}:Telegram 用户已成功移除。"
 "enableSuccess" = "✅ {{ .Email }}:已成功启用。"
 "disableSuccess" = "✅ {{ .Email }}:已成功禁用。"
-"askToAddUserId" = "未找到您的配置!\r\n请向管理员询问,在您的配置中使用您的 Telegram 用户ID。\r\n\r\n您的用户ID:<b>{{ .TgUserID }}</b>"
-"askToAddUserName" = "未找到您的配置!\r\n请向管理员询问,在您的配置中使用您的 Telegram 用户名或用户ID。\r\n\r\n您的用户名:<b>@{{ .TgUserName }}</b>\r\n\r\n您的用户ID:<b>{{ .TgUserID }}</b>"
+"askToAddUserId" = "未找到您的配置!\r\n请向管理员询问,在您的配置中使用您的 Telegram 用户ID。\r\n\r\n您的用户ID:<code>{{ .TgUserID }}</code>"

+ 4 - 4
web/web.go

@@ -240,11 +240,11 @@ func (s *Server) startTask() {
 	if err != nil {
 		logger.Warning("start xray failed:", err)
 	}
-	// Check whether xray is running every 30 seconds
-	s.cron.AddJob("@every 30s", job.NewCheckXrayRunningJob())
+	// Check whether xray is running every second
+	s.cron.AddJob("@every 1s", job.NewCheckXrayRunningJob())
 
-	// Check if xray needs to be restarted
-	s.cron.AddFunc("@every 10s", func() {
+	// Check if xray needs to be restarted every 30 seconds
+	s.cron.AddFunc("@every 30s", func() {
 		if s.xrayService.IsNeedRestartAndSetFalse() {
 			err := s.xrayService.RestartXray(false)
 			if err != nil {

+ 13 - 1
xray/process.go

@@ -41,12 +41,24 @@ func GetIPLimitLogPath() string {
 	return config.GetLogFolder() + "/3xipl.log"
 }
 
+func GetIPLimitPrevLogPath() string {
+	return config.GetLogFolder() + "/3xipl.prev.log"
+}
+
 func GetIPLimitBannedLogPath() string {
 	return config.GetLogFolder() + "/3xipl-banned.log"
 }
 
+func GetIPLimitBannedPrevLogPath() string {
+	return config.GetLogFolder() + "/3xipl-banned.prev.log"
+}
+
 func GetAccessPersistentLogPath() string {
-	return config.GetLogFolder() + "/3xipl-access-persistent.log"
+	return config.GetLogFolder() + "/3xipl-ap.log"
+}
+
+func GetAccessPersistentPrevLogPath() string {
+	return config.GetLogFolder() + "/3xipl-ap.prev.log"
 }
 
 func GetAccessLogPath() string {