diff --git a/.gitignore b/.gitignore index 6277cfc9..158f4ee1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,15 @@ .idea .vscode +.cache +.sync* +*.tar.gz +access.log +error.log tmp +main backup/ bin/ dist/ -x-ui-*.tar.gz -/x-ui -/release.sh -.sync* -main release/ -access.log -error.log -.cache +/release.sh +/x-ui diff --git a/README.md b/README.md index d39494ca..54966a43 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,7 @@ Reference syntax: | `GET` | `"/list"` | Get all inbounds | | `GET` | `"/get/:id"` | Get inbound with inbound.id | | `GET` | `"/getClientTraffics/:email"` | Get Client Traffics with email | +| `GET` | `"/createbackup"` | Telegram bot sends backup to admins | | `POST` | `"/add"` | Add inbound | | `POST` | `"/del/:id"` | Delete Inbound | | `POST` | `"/update/:id"` | Update Inbound | diff --git a/web/assets/js/model/models.js b/web/assets/js/model/models.js index a3fd2633..e1fb5d02 100644 --- a/web/assets/js/model/models.js +++ b/web/assets/js/model/models.js @@ -181,6 +181,7 @@ class AllSetting { this.tgRunTime = "@daily"; this.tgBotBackup = false; this.tgCpu = ""; + this.tgLang = ""; this.xrayTemplateConfig = ""; this.secretEnable = false; diff --git a/web/controller/api.go b/web/controller/api.go index 17073345..32c639f8 100644 --- a/web/controller/api.go +++ b/web/controller/api.go @@ -102,5 +102,5 @@ func (a *APIController) delDepletedClients(c *gin.Context) { } func (a *APIController) createBackup(c *gin.Context) { - a.Tgbot.SendBackUP(c) + a.Tgbot.SendBackupToAdmins() } diff --git a/web/controller/base.go b/web/controller/base.go index 98e1831c..674a195d 100644 --- a/web/controller/base.go +++ b/web/controller/base.go @@ -2,6 +2,8 @@ package controller import ( "net/http" + "x-ui/logger" + "x-ui/web/locale" "x-ui/web/session" "github.com/gin-gonic/gin" @@ -13,7 +15,7 @@ type BaseController struct { func (a *BaseController) checkLogin(c *gin.Context) { if !session.IsLogin(c) { if isAjax(c) { - pureJsonMsg(c, false, I18n(c, "pages.login.loginAgain")) + pureJsonMsg(c, false, I18nWeb(c, "pages.login.loginAgain")) } else { c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path")) } @@ -23,11 +25,13 @@ func (a *BaseController) checkLogin(c *gin.Context) { } } -func I18n(c *gin.Context, name string) string { - anyfunc, _ := c.Get("I18n") - i18n, _ := anyfunc.(func(key string, params ...string) (string, error)) - - message, _ := i18n(name) - - return message +func I18nWeb(c *gin.Context, name string, params ...string) string { + anyfunc, funcExists := c.Get("I18n") + if !funcExists { + logger.Warning("I18n function not exists in gin context!") + return "" + } + i18nFunc, _ := anyfunc.(func(i18nType locale.I18nType, key string, keyParams ...string) string) + msg := i18nFunc(locale.Web, name, params...) + return msg } diff --git a/web/controller/inbound.go b/web/controller/inbound.go index 8360cf62..d13e40bc 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -60,7 +60,7 @@ func (a *InboundController) getInbounds(c *gin.Context) { user := session.GetLoginUser(c) inbounds, err := a.inboundService.GetInbounds(user.Id) if err != nil { - jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err) + jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err) return } jsonObj(c, inbounds, nil) @@ -68,12 +68,12 @@ func (a *InboundController) getInbounds(c *gin.Context) { func (a *InboundController) getInbound(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - jsonMsg(c, I18n(c, "get"), err) + jsonMsg(c, I18nWeb(c, "get"), err) return } inbound, err := a.inboundService.GetInbound(id) if err != nil { - jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err) + jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err) return } jsonObj(c, inbound, nil) @@ -93,7 +93,7 @@ func (a *InboundController) addInbound(c *gin.Context) { inbound := &model.Inbound{} err := c.ShouldBind(inbound) if err != nil { - jsonMsg(c, I18n(c, "pages.inbounds.create"), err) + jsonMsg(c, I18nWeb(c, "pages.inbounds.create"), err) return } user := session.GetLoginUser(c) @@ -101,7 +101,7 @@ func (a *InboundController) addInbound(c *gin.Context) { inbound.Enable = true inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) inbound, err = a.inboundService.AddInbound(inbound) - jsonMsgObj(c, I18n(c, "pages.inbounds.create"), inbound, err) + jsonMsgObj(c, I18nWeb(c, "pages.inbounds.create"), inbound, err) if err == nil { a.xrayService.SetToNeedRestart() } @@ -110,11 +110,11 @@ func (a *InboundController) addInbound(c *gin.Context) { func (a *InboundController) delInbound(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - jsonMsg(c, I18n(c, "delete"), err) + jsonMsg(c, I18nWeb(c, "delete"), err) return } err = a.inboundService.DelInbound(id) - jsonMsgObj(c, I18n(c, "delete"), id, err) + jsonMsgObj(c, I18nWeb(c, "delete"), id, err) if err == nil { a.xrayService.SetToNeedRestart() } @@ -123,7 +123,7 @@ func (a *InboundController) delInbound(c *gin.Context) { func (a *InboundController) updateInbound(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - jsonMsg(c, I18n(c, "pages.inbounds.update"), err) + jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err) return } inbound := &model.Inbound{ @@ -131,11 +131,11 @@ func (a *InboundController) updateInbound(c *gin.Context) { } err = c.ShouldBind(inbound) if err != nil { - jsonMsg(c, I18n(c, "pages.inbounds.update"), err) + jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err) return } inbound, err = a.inboundService.UpdateInbound(inbound) - jsonMsgObj(c, I18n(c, "pages.inbounds.update"), inbound, err) + jsonMsgObj(c, I18nWeb(c, "pages.inbounds.update"), inbound, err) if err == nil { a.xrayService.SetToNeedRestart() } @@ -165,7 +165,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) { data := &model.Inbound{} err := c.ShouldBind(data) if err != nil { - jsonMsg(c, I18n(c, "pages.inbounds.update"), err) + jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err) return } @@ -183,7 +183,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) { func (a *InboundController) delInboundClient(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - jsonMsg(c, I18n(c, "pages.inbounds.update"), err) + jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err) return } clientId := c.Param("clientId") @@ -205,7 +205,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) { inbound := &model.Inbound{} err := c.ShouldBind(inbound) if err != nil { - jsonMsg(c, I18n(c, "pages.inbounds.update"), err) + jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err) return } @@ -223,7 +223,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) { func (a *InboundController) resetClientTraffic(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - jsonMsg(c, I18n(c, "pages.inbounds.update"), err) + jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err) return } email := c.Param("email") @@ -251,7 +251,7 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) { func (a *InboundController) resetAllClientTraffics(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - jsonMsg(c, I18n(c, "pages.inbounds.update"), err) + jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err) return } @@ -266,7 +266,7 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) { func (a *InboundController) delDepletedClients(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - jsonMsg(c, I18n(c, "pages.inbounds.update"), err) + jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err) return } err = a.inboundService.DelDepletedClients(id) diff --git a/web/controller/index.go b/web/controller/index.go index ac2ceca1..0254106c 100644 --- a/web/controller/index.go +++ b/web/controller/index.go @@ -49,26 +49,27 @@ func (a *IndexController) login(c *gin.Context) { var form LoginForm err := c.ShouldBind(&form) if err != nil { - pureJsonMsg(c, false, I18n(c, "pages.login.toasts.invalidFormData")) + pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.invalidFormData")) return } if form.Username == "" { - pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyUsername")) + pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyUsername")) return } if form.Password == "" { - pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyPassword")) + pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyPassword")) return } + user := a.userService.CheckUser(form.Username, form.Password, form.LoginSecret) timeStr := time.Now().Format("2006-01-02 15:04:05") if user == nil { - a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0) logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password) - pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword")) + a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0) + pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword")) return } else { - logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c)) + logger.Infof("%s login success, Ip Address: %s\n", form.Username, getRemoteIp(c)) a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1) } @@ -86,7 +87,7 @@ func (a *IndexController) login(c *gin.Context) { err = session.SetLoginUser(c, user) logger.Info("user", user.Id, "login success") - jsonMsg(c, I18n(c, "pages.login.toasts.successLogin"), err) + jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), err) } func (a *IndexController) logout(c *gin.Context) { diff --git a/web/controller/server.go b/web/controller/server.go index 2db6e7fd..cc4eaacc 100644 --- a/web/controller/server.go +++ b/web/controller/server.go @@ -81,7 +81,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) { versions, err := a.serverService.GetXrayVersions() if err != nil { - jsonMsg(c, I18n(c, "getVersion"), err) + jsonMsg(c, I18nWeb(c, "getVersion"), err) return } @@ -94,7 +94,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) { func (a *ServerController) installXray(c *gin.Context) { version := c.Param("version") err := a.serverService.UpdateXray(version) - jsonMsg(c, I18n(c, "install")+" xray", err) + jsonMsg(c, I18nWeb(c, "install")+" xray", err) } func (a *ServerController) stopXrayService(c *gin.Context) { diff --git a/web/controller/setting.go b/web/controller/setting.go index 3aed69e6..226b7975 100644 --- a/web/controller/setting.go +++ b/web/controller/setting.go @@ -49,7 +49,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) { func (a *SettingController) getAllSetting(c *gin.Context) { allSetting, err := a.settingService.GetAllSetting() if err != nil { - jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) return } jsonObj(c, allSetting, nil) @@ -58,7 +58,7 @@ func (a *SettingController) getAllSetting(c *gin.Context) { func (a *SettingController) getDefaultJsonConfig(c *gin.Context) { defaultJsonConfig, err := a.settingService.GetDefaultJsonConfig() if err != nil { - jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) return } jsonObj(c, defaultJsonConfig, nil) @@ -67,22 +67,22 @@ func (a *SettingController) getDefaultJsonConfig(c *gin.Context) { func (a *SettingController) getDefaultSettings(c *gin.Context) { expireDiff, err := a.settingService.GetExpireDiff() if err != nil { - jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) return } trafficDiff, err := a.settingService.GetTrafficDiff() if err != nil { - jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) return } defaultCert, err := a.settingService.GetCertFile() if err != nil { - jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) return } defaultKey, err := a.settingService.GetKeyFile() if err != nil { - jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) return } result := map[string]interface{}{ @@ -98,27 +98,27 @@ func (a *SettingController) updateSetting(c *gin.Context) { allSetting := &entity.AllSetting{} err := c.ShouldBind(allSetting) if err != nil { - jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) return } err = a.settingService.UpdateAllSetting(allSetting) - jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) } func (a *SettingController) updateUser(c *gin.Context) { form := &updateUserForm{} err := c.ShouldBind(form) if err != nil { - jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) return } user := session.GetLoginUser(c) if user.Username != form.OldUsername || user.Password != form.OldPassword { - jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.originalUserPassIncorrect"))) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect"))) return } if form.NewUsername == "" || form.NewPassword == "" { - jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.userPassMustBeNotEmpty"))) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.userPassMustBeNotEmpty"))) return } err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword) @@ -127,19 +127,19 @@ func (a *SettingController) updateUser(c *gin.Context) { user.Password = form.NewPassword session.SetLoginUser(c, user) } - jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err) } func (a *SettingController) restartPanel(c *gin.Context) { err := a.panelService.RestartPanel(time.Second * 3) - jsonMsg(c, I18n(c, "pages.settings.restartPanel"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.restartPanel"), err) } func (a *SettingController) updateSecret(c *gin.Context) { form := &updateSecretForm{} err := c.ShouldBind(form) if err != nil { - jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) } user := session.GetLoginUser(c) err = a.userService.UpdateUserSecret(user.Id, form.LoginSecret) @@ -147,7 +147,7 @@ func (a *SettingController) updateSecret(c *gin.Context) { user.LoginSecret = form.LoginSecret session.SetLoginUser(c, user) } - jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err) } func (a *SettingController) getUserSecret(c *gin.Context) { diff --git a/web/controller/util.go b/web/controller/util.go index dc5c83b0..da77189b 100644 --- a/web/controller/util.go +++ b/web/controller/util.go @@ -38,12 +38,12 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) { if err == nil { m.Success = true if msg != "" { - m.Msg = msg + I18n(c, "success") + m.Msg = msg + I18nWeb(c, "success") } } else { m.Success = false - m.Msg = msg + I18n(c, "fail") + ": " + err.Error() - logger.Warning(msg+I18n(c, "fail")+": ", err) + m.Msg = msg + I18nWeb(c, "fail") + ": " + err.Error() + logger.Warning(msg+I18nWeb(c, "fail")+": ", err) } c.JSON(http.StatusOK, m) } diff --git a/web/entity/entity.go b/web/entity/entity.go index b370b7ba..52f26769 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -41,6 +41,7 @@ type AllSetting struct { TgRunTime string `json:"tgRunTime" form:"tgRunTime"` TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"` TgCpu int `json:"tgCpu" form:"tgCpu"` + TgLang string `json:"tgLang" form:"tgLang"` XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"` TimeLocation string `json:"timeLocation" form:"timeLocation"` SecretEnable bool `json:"secretEnable" form:"secretEnable"` diff --git a/web/global/hashStorage.go b/web/global/hashStorage.go new file mode 100644 index 00000000..9dfea169 --- /dev/null +++ b/web/global/hashStorage.go @@ -0,0 +1,82 @@ +package global + +import ( + "crypto/md5" + "encoding/hex" + "regexp" + "sync" + "time" +) + +type HashEntry struct { + Hash string + Value string + Timestamp time.Time +} + +type HashStorage struct { + sync.RWMutex + Data map[string]HashEntry + Expiration time.Duration + +} + +func NewHashStorage(expiration time.Duration) *HashStorage { + return &HashStorage{ + Data: make(map[string]HashEntry), + Expiration: expiration, + } +} + +func (h *HashStorage) SaveHash(query string) string { + h.Lock() + defer h.Unlock() + + md5Hash := md5.Sum([]byte(query)) + md5HashString := hex.EncodeToString(md5Hash[:]) + + entry := HashEntry{ + Hash: md5HashString, + Value: query, + Timestamp: time.Now(), + } + + h.Data[md5HashString] = entry + + return md5HashString +} + + +func (h *HashStorage) GetValue(hash string) (string, bool) { + h.RLock() + defer h.RUnlock() + + entry, exists := h.Data[hash] + + return entry.Value, exists +} + +func (h *HashStorage) IsMD5(hash string) bool { + match, _ := regexp.MatchString("^[a-f0-9]{32}$", hash) + return match +} + +func (h *HashStorage) RemoveExpiredHashes() { + h.Lock() + defer h.Unlock() + + now := time.Now() + + for hash, entry := range h.Data { + if now.Sub(entry.Timestamp) > h.Expiration { + delete(h.Data, hash) + } + } +} + +func (h *HashStorage) Reset() { + h.Lock() + defer h.Unlock() + + h.Data = make(map[string]HashEntry) +} diff --git a/web/html/xui/form/tls_settings.html b/web/html/xui/form/tls_settings.html index 91642727..35f101ca 100644 --- a/web/html/xui/form/tls_settings.html +++ b/web/html/xui/form/tls_settings.html @@ -10,7 +10,7 @@ Reality @@ -22,7 +22,7 @@ XTLS @@ -100,7 +100,7 @@ - + diff --git a/web/html/xui/inbounds.html b/web/html/xui/inbounds.html index 6fdf0c43..a66e84a9 100644 --- a/web/html/xui/inbounds.html +++ b/web/html/xui/inbounds.html @@ -105,6 +105,10 @@ + + {{ i18n "none" }} @@ -112,10 +116,6 @@ {{ i18n "depleted" }} {{ i18n "depletingSoon" }} - - .ant-tabs-top-bar { background: white; } + + .alert-msg { + color: rgb(194, 117, 18); + font-weight: bold; + font-size: 20px; + margin-top: 5px; + padding: 16px 6px; + text-align: center; + border-bottom: 1px solid; + } + + .alert-msg > i { + color: inherit; + font-size: 24px; + } + + .collapse-title { + color: inherit; + font-weight: bold; + font-size: 18px; + padding: 10px 20px; + border-bottom: 2px solid; + } + + .collapse-title > i { + color: inherit; + font-size: 24px; + } @@ -35,8 +63,14 @@ {{ i18n "pages.settings.save" }} {{ i18n "pages.settings.restartPanel" }} - + + +

+ + {{ i18n "pages.settings.infoDesc" }} +

+
@@ -72,12 +106,6 @@ - -

- - {{ i18n "pages.settings.infoDesc" }} -

-
@@ -144,8 +172,8 @@ {{ i18n "pages.settings.templates.title"}} -

- +

+ {{ i18n "pages.settings.infoDesc" }}

@@ -154,8 +182,8 @@ -

- +

+ {{ i18n "pages.settings.templates.generalConfigsDesc" }}

@@ -199,8 +227,8 @@
-

- +

+ {{ i18n "pages.settings.templates.blockConfigsDesc" }}

@@ -212,8 +240,8 @@
-

- +

+ {{ i18n "pages.settings.templates.blockCountryConfigsDesc" }}

@@ -226,8 +254,8 @@
-

- +

+ {{ i18n "pages.settings.templates.directCountryConfigsDesc" }}

@@ -240,8 +268,8 @@
-

- +

+ {{ i18n "pages.settings.templates.ipv4ConfigsDesc" }}

@@ -250,8 +278,8 @@
-

- +

+ {{ i18n "pages.settings.templates.warpConfigsDesc" }}

@@ -262,8 +290,8 @@
-

- +

+ {{ i18n "pages.settings.templates.manualListsDesc" }}

@@ -271,6 +299,8 @@ + +
@@ -295,6 +325,12 @@ + +

+ + {{ i18n "pages.settings.infoDesc" }} +

+
@@ -302,13 +338,30 @@ + + + + + + + + + + + - -

- - {{ i18n "pages.settings.infoDesc" }} -

-
@@ -452,7 +505,12 @@ if (msg.success) { this.loading(true); await PromiseUtil.sleep(5000); - window.location.replace(this.allSetting.webBasePath + "panel/settings"); + let protocol = "http://"; + if (this.allSetting.webCertFile !== "") { + protocol = "https://"; + } + const { host, pathname } = window.location; + window.location.replace(protocol + host + this.allSetting.webBasePath + pathname.slice(1)); } }, async fetchUserSecret() { @@ -584,30 +642,30 @@ computed: { templateSettings: { get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null; }, - set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2) }, + set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2); }, }, inboundSettings: { get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; }, set: function (newValue) { newTemplateSettings = this.templateSettings; - newTemplateSettings.inbounds = JSON.parse(newValue) - this.templateSettings = newTemplateSettings + newTemplateSettings.inbounds = JSON.parse(newValue); + this.templateSettings = newTemplateSettings; }, }, outboundSettings: { get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; }, set: function (newValue) { newTemplateSettings = this.templateSettings; - newTemplateSettings.outbounds = JSON.parse(newValue) - this.templateSettings = newTemplateSettings + newTemplateSettings.outbounds = JSON.parse(newValue); + this.templateSettings = newTemplateSettings; }, }, routingRuleSettings: { get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; }, set: function (newValue) { newTemplateSettings = this.templateSettings; - newTemplateSettings.routing.rules = JSON.parse(newValue) - this.templateSettings = newTemplateSettings + newTemplateSettings.routing.rules = JSON.parse(newValue); + this.templateSettings = newTemplateSettings; }, }, freedomStrategy: { @@ -682,6 +740,24 @@ this.syncRulesWithOutbound("direct", this.directSettings); } }, + ipv4Domains: { + get: function () { + return this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" }); + }, + set: function (newValue) { + this.templateRuleSetter({ outboundTag: "IPv4", property: "domain", data: newValue }); + this.syncRulesWithOutbound("IPv4", this.ipv4Settings); + } + }, + warpDomains: { + get: function () { + return this.templateRuleGetter({ outboundTag: "WARP", property: "domain" }); + }, + set: function (newValue) { + this.templateRuleSetter({ outboundTag: "WARP", property: "domain", data: newValue }); + this.syncRulesWithOutbound("WARP", this.warpSettings); + } + }, manualBlockedIPs: { get: function () { return JSON.stringify(this.blockedIPs, null, 2); }, set: debounce(function (value) { this.blockedIPs = JSON.parse(value); }, 1000) @@ -698,6 +774,14 @@ get: function () { return JSON.stringify(this.directDomains, null, 2); }, set: debounce(function (value) { this.directDomains = JSON.parse(value); }, 1000) }, + manualIPv4Domains: { + get: function () { return JSON.stringify(this.ipv4Domains, null, 2); }, + set: debounce(function (value) { this.ipv4Domains = JSON.parse(value); }, 1000) + }, + manualWARPDomains: { + get: function () { return JSON.stringify(this.warpDomains, null, 2); }, + set: debounce(function (value) { this.warpDomains = JSON.parse(value); }, 1000) + }, torrentSettings: { get: function () { return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols); @@ -763,40 +847,26 @@ }, GoogleIPv4Settings: { get: function () { - return doAllItemsExist(this.settingsData.domains.google, this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" })); + return doAllItemsExist(this.settingsData.domains.google, this.ipv4Domains); }, set: function (newValue) { - oldData = this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" }); if (newValue) { - oldData = [...oldData, ...this.settingsData.domains.google]; + this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.google]; } else { - oldData = oldData.filter(data => !this.settingsData.domains.google.includes(data)) + this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.google.includes(data)); } - this.templateRuleSetter({ - outboundTag: "IPv4", - property: "domain", - data: oldData - }); - this.syncRulesWithOutbound("IPv4", this.ipv4Settings); }, }, NetflixIPv4Settings: { get: function () { - return doAllItemsExist(this.settingsData.domains.netflix, this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" })); + return doAllItemsExist(this.settingsData.domains.netflix, this.ipv4Domains); }, set: function (newValue) { - oldData = this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" }); if (newValue) { - oldData = [...oldData, ...this.settingsData.domains.netflix]; + this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.netflix]; } else { - oldData = oldData.filter(data => !this.settingsData.domains.netflix.includes(data)) + this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.netflix.includes(data)); } - this.templateRuleSetter({ - outboundTag: "IPv4", - property: "domain", - data: oldData - }); - this.syncRulesWithOutbound("IPv4", this.ipv4Settings); }, }, IRIpSettings: { @@ -945,78 +1015,50 @@ }, GoogleWARPSettings: { get: function () { - return doAllItemsExist(this.settingsData.domains.google, this.templateRuleGetter({ outboundTag: "WARP", property: "domain" })); + return doAllItemsExist(this.settingsData.domains.google, this.warpDomains); }, set: function (newValue) { - oldData = this.templateRuleGetter({ outboundTag: "WARP", property: "domain" }); if (newValue) { - oldData = [...oldData, ...this.settingsData.domains.google]; + this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.google]; } else { - oldData = oldData.filter(data => !this.settingsData.domains.google.includes(data)) + this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.google.includes(data)); } - this.templateRuleSetter({ - outboundTag: "WARP", - property: "domain", - data: oldData - }); - this.syncRulesWithOutbound("WARP", this.warpSettings); }, }, OpenAIWARPSettings: { get: function () { - return doAllItemsExist(this.settingsData.domains.openai, this.templateRuleGetter({ outboundTag: "WARP", property: "domain" })); + return doAllItemsExist(this.settingsData.domains.openai, this.warpDomains); }, set: function (newValue) { - oldData = this.templateRuleGetter({ outboundTag: "WARP", property: "domain" }); if (newValue) { - oldData = [...oldData, ...this.settingsData.domains.openai]; + this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.openai]; } else { - oldData = oldData.filter(data => !this.settingsData.domains.openai.includes(data)) + this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.openai.includes(data)); } - this.templateRuleSetter({ - outboundTag: "WARP", - property: "domain", - data: oldData - }); - this.syncRulesWithOutbound("WARP", this.warpSettings); }, }, NetflixWARPSettings: { get: function () { - return doAllItemsExist(this.settingsData.domains.netflix, this.templateRuleGetter({ outboundTag: "WARP", property: "domain" })); + return doAllItemsExist(this.settingsData.domains.netflix, this.warpDomains); }, set: function (newValue) { - oldData = this.templateRuleGetter({ outboundTag: "WARP", property: "domain" }); if (newValue) { - oldData = [...oldData, ...this.settingsData.domains.netflix]; + this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.netflix]; } else { - oldData = oldData.filter(data => !this.settingsData.domains.netflix.includes(data)) + this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.netflix.includes(data)); } - this.templateRuleSetter({ - outboundTag: "WARP", - property: "domain", - data: oldData - }); - this.syncRulesWithOutbound("WARP", this.warpSettings); }, }, SpotifyWARPSettings: { get: function () { - return doAllItemsExist(this.settingsData.domains.spotify, this.templateRuleGetter({ outboundTag: "WARP", property: "domain" })); + return doAllItemsExist(this.settingsData.domains.spotify, this.warpDomains); }, set: function (newValue) { - oldData = this.templateRuleGetter({ outboundTag: "WARP", property: "domain" }); if (newValue) { - oldData = [...oldData, ...this.settingsData.domains.spotify]; + this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.spotify]; } else { - oldData = oldData.filter(data => !this.settingsData.domains.spotify.includes(data)) + this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.spotify.includes(data)); } - this.templateRuleSetter({ - outboundTag: "WARP", - property: "domain", - data: oldData - }); - this.syncRulesWithOutbound("WARP", this.warpSettings); }, }, }, diff --git a/web/job/check_cpu_usage.go b/web/job/check_cpu_usage.go index cfc86b60..74f6a544 100644 --- a/web/job/check_cpu_usage.go +++ b/web/job/check_cpu_usage.go @@ -1,7 +1,7 @@ package job import ( - "fmt" + "strconv" "time" "x-ui/web/service" @@ -24,7 +24,10 @@ func (j *CheckCpuJob) Run() { // get latest status of server percent, err := cpu.Percent(1*time.Second, false) if err == nil && percent[0] > float64(threshold) { - msg := fmt.Sprintf("🔴 CPU usage %.2f%% is more than threshold %d%%", percent[0], threshold) + msg := j.tgbotService.I18nBot("tgbot.messages.cpuThreshold", + "Percent=="+strconv.FormatFloat(percent[0], 'f', 2, 64), + "Threshold=="+strconv.Itoa(threshold)) + j.tgbotService.SendMsgToTgbotAdmins(msg) } } diff --git a/web/job/check_hash_storage.go b/web/job/check_hash_storage.go new file mode 100644 index 00000000..468aa2e0 --- /dev/null +++ b/web/job/check_hash_storage.go @@ -0,0 +1,19 @@ +package job + +import ( + "x-ui/web/service" +) + +type CheckHashStorageJob struct { + tgbotService service.Tgbot +} + +func NewCheckHashStorageJob() *CheckHashStorageJob { + return new(CheckHashStorageJob) +} + +// Here Run is an interface method of the Job interface +func (j *CheckHashStorageJob) Run() { + // Remove expired hashes from storage + j.tgbotService.GetHashStorage().RemoveExpiredHashes() +} diff --git a/web/locale/locale.go b/web/locale/locale.go new file mode 100644 index 00000000..9a4357c3 --- /dev/null +++ b/web/locale/locale.go @@ -0,0 +1,144 @@ +package locale + +import ( + "embed" + "io/fs" + "strings" + "x-ui/logger" + + "github.com/gin-gonic/gin" + "github.com/nicksnyder/go-i18n/v2/i18n" + "github.com/pelletier/go-toml/v2" + "golang.org/x/text/language" +) + +var i18nBundle *i18n.Bundle +var LocalizerWeb *i18n.Localizer +var LocalizerBot *i18n.Localizer + +type I18nType string + +const ( + Bot I18nType = "bot" + Web I18nType = "web" +) + +type SettingService interface { + GetTgLang() (string, error) +} + +func InitLocalizer(i18nFS embed.FS, settingService SettingService) error { + // set default bundle to english + i18nBundle = i18n.NewBundle(language.English) + i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) + + // parse files + if err := parseTranslationFiles(i18nFS, i18nBundle); err != nil { + return err + } + + // setup bot locale + if err := initTGBotLocalizer(settingService); err != nil { + return err + } + + return nil +} + +func createTemplateData(params []string, seperator ...string) map[string]interface{} { + var sep string = "==" + if len(seperator) > 0 { + sep = seperator[0] + } + + templateData := make(map[string]interface{}) + for _, param := range params { + parts := strings.SplitN(param, sep, 2) + templateData[parts[0]] = parts[1] + } + + return templateData +} + +func I18n(i18nType I18nType, key string, params ...string) string { + var localizer *i18n.Localizer + + switch i18nType { + case "bot": + localizer = LocalizerBot + case "web": + localizer = LocalizerWeb + default: + logger.Errorf("Invalid type for I18n: %s", i18nType) + return "" + } + + templateData := createTemplateData(params) + + msg, err := localizer.Localize(&i18n.LocalizeConfig{ + MessageID: key, + TemplateData: templateData, + }) + + if err != nil { + logger.Errorf("Failed to localize message: %v", err) + return "" + } + + return msg +} + +func initTGBotLocalizer(settingService SettingService) error { + botLang, err := settingService.GetTgLang() + if err != nil { + return err + } + + LocalizerBot = i18n.NewLocalizer(i18nBundle, botLang) + return nil +} + +func LocalizerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + var lang string + + if cookie, err := c.Request.Cookie("lang"); err == nil { + lang = cookie.Value + } else { + lang = c.GetHeader("Accept-Language") + } + + LocalizerWeb = i18n.NewLocalizer(i18nBundle, lang) + + c.Set("localizer", LocalizerWeb) + c.Set("I18n", I18n) + c.Next() + } +} + +func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error { + err := fs.WalkDir(i18nFS, "translation", + func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + data, err := i18nFS.ReadFile(path) + if err != nil { + return err + } + + _, err = i18nBundle.ParseMessageFileBytes(data, path) + return err + }) + + if err != nil { + return err + } + + return nil +} diff --git a/web/network/autp_https_conn.go b/web/network/auto_https_conn.go similarity index 100% rename from web/network/autp_https_conn.go rename to web/network/auto_https_conn.go diff --git a/web/service/setting.go b/web/service/setting.go index d3072252..fec324af 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -39,6 +39,7 @@ var defaultValueMap = map[string]string{ "tgRunTime": "@daily", "tgBotBackup": "false", "tgCpu": "0", + "tgLang": "en-US", "secretEnable": "false", } @@ -256,6 +257,10 @@ func (s *SettingService) GetTgCpu() (int, error) { return s.getInt("tgCpu") } +func (s *SettingService) GetTgLang() (string, error) { + return s.getString("tgLang") +} + func (s *SettingService) GetPort() (int, error) { return s.getInt("webPort") } diff --git a/web/service/sub.go b/web/service/sub.go index f39fdb1e..b9ea49bd 100644 --- a/web/service/sub.go +++ b/web/service/sub.go @@ -603,7 +603,8 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st } } encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password) - return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, clients[clientIndex].Email) + remark := fmt.Sprintf("%s-%s", inbound.Remark, clients[clientIndex].Email) + return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, remark) } func searchKey(data interface{}, key string) (interface{}, bool) { diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 0b301e29..9cd88516 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -1,6 +1,7 @@ package service import ( + "embed" "fmt" "net" "os" @@ -11,9 +12,10 @@ import ( "x-ui/database/model" "x-ui/logger" "x-ui/util/common" + "x-ui/web/global" + "x-ui/web/locale" "x-ui/xray" - "github.com/gin-gonic/gin" "github.com/mymmrac/telego" th "github.com/mymmrac/telego/telegohandler" tu "github.com/mymmrac/telego/telegoutil" @@ -23,6 +25,8 @@ var bot *telego.Bot var botHandler *th.BotHandler var adminIds []int64 var isRunning bool +var hostname string +var hashStorage *global.HashStorage type LoginStatus byte @@ -43,7 +47,24 @@ func (t *Tgbot) NewTgbot() *Tgbot { return new(Tgbot) } -func (t *Tgbot) Start() error { +func (t *Tgbot) I18nBot(name string, params ...string) string { + return locale.I18n(locale.Bot, name, params...) +} + +func (t *Tgbot) GetHashStorage() *global.HashStorage { + return hashStorage +} + +func (t *Tgbot) Start(i18nFS embed.FS) error { + err := locale.InitLocalizer(i18nFS, &t.settingService) + if err != nil { + return err + } + + // init hash storage => store callback queries + hashStorage = global.NewHashStorage(20 * time.Minute) + + t.SetHostname() tgBottoken, err := t.settingService.GetTgBotToken() if err != nil || tgBottoken == "" { logger.Warning("Get TgBotToken failed:", err) @@ -81,10 +102,20 @@ func (t *Tgbot) Start() error { return nil } -func (t *Tgbot) IsRunnging() bool { +func (t *Tgbot) IsRunning() bool { return isRunning } +func (t *Tgbot) SetHostname() { + host, err := os.Hostname() + if err != nil { + logger.Error("get hostname error:", err) + hostname = "" + return + } + hostname = host +} + func (t *Tgbot) Stop() { botHandler.Stop() bot.StopLongPolling() @@ -93,6 +124,28 @@ func (t *Tgbot) Stop() { adminIds = nil } +func (t *Tgbot) encodeQuery(query string) string { + // NOTE: we only need to hash for more than 64 chars + if len(query) <= 64 { + return query + } + + return hashStorage.SaveHash(query) +} + +func (t *Tgbot) decodeQuery(query string) (string, error) { + if !hashStorage.IsMD5(query) { + return query, nil + } + + decoded, exists := hashStorage.GetValue(query) + if !exists { + return "", common.NewError("hash not found in storage!") + } + + return decoded, nil +} + func (t *Tgbot) OnReceive() { params := telego.GetUpdatesParams{ Timeout: 10, @@ -102,31 +155,31 @@ func (t *Tgbot) OnReceive() { botHandler, _ = th.NewBotHandler(bot, updates) - botHandler.HandleMessage(func(bot *telego.Bot, message telego.Message) { - t.SendMsgToTgbot(message.Chat.ID, "Custom Keyboard Closed!", tu.ReplyKeyboardRemove()) - }, th.TextEqual("❌ Close Keyboard")) + botHandler.HandleMessage(func(_ *telego.Bot, message telego.Message) { + t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove()) + }, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard"))) - botHandler.HandleMessage(func(bot *telego.Bot, message telego.Message) { + botHandler.HandleMessage(func(_ *telego.Bot, message telego.Message) { t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID)) }, th.AnyCommand()) - botHandler.HandleCallbackQuery(func(bot *telego.Bot, query telego.CallbackQuery) { + botHandler.HandleCallbackQuery(func(_ *telego.Bot, query telego.CallbackQuery) { t.asnwerCallback(&query, checkAdmin(query.From.ID)) }, th.AnyCallbackQueryWithMessage()) - botHandler.HandleMessage(func(bot *telego.Bot, message telego.Message) { + botHandler.HandleMessage(func(_ *telego.Bot, message telego.Message) { if message.UserShared != nil { if checkAdmin(message.From.ID) { err := t.inboundService.SetClientTelegramUserID(message.UserShared.RequestID, strconv.FormatInt(message.UserShared.UserID, 10)) - var output string + output := "" if err != nil { - output = "❌ Error in user selection!" + output += t.I18nBot("tgbot.messages.selectUserFailed") } else { - output = "✅ Telegram User saved." + output += t.I18nBot("tgbot.messages.userSaved") } t.SendMsgToTgbot(message.Chat.ID, output, tu.ReplyKeyboardRemove()) } else { - t.SendMsgToTgbot(message.Chat.ID, "No result!", tu.ReplyKeyboardRemove()) + t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.noResult"), tu.ReplyKeyboardRemove()) } } }, th.AnyMessage()) @@ -142,16 +195,16 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo // Extract the command from the Message. switch command { case "help": - msg = "This bot is providing you some specefic data from the server.\n\n Please choose:" + msg += t.I18nBot("tgbot.commands.help") + msg += t.I18nBot("tgbot.commands.pleaseChoose") case "start": - msg = "Hello " + message.From.FirstName + " 👋" + msg += t.I18nBot("tgbot.commands.start", "Firstname=="+message.From.FirstName) if isAdmin { - hostname, _ := os.Hostname() - msg += "\nWelcome to " + hostname + " management bot" + msg += t.I18nBot("tgbot.commands.welcome", "Hostname=="+hostname) } - msg += "\n\nI can do some magics for you, please choose:" + msg += "\n\n" + t.I18nBot("tgbot.commands.pleaseChoose") case "status": - msg = "bot is ok ✅" + msg += t.I18nBot("tgbot.commands.status") case "usage": if len(commandArgs) > 0 { if isAdmin { @@ -160,16 +213,16 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo t.searchForClient(chatId, commandArgs[0]) } } else { - msg = "❗Please provide a text for search!" + msg += t.I18nBot("tgbot.commands.usage") } case "inbound": if isAdmin && len(commandArgs) > 0 { t.searchInbound(chatId, commandArgs[0]) } else { - msg = "❗ Unknown command" + msg += t.I18nBot("tgbot.commands.unknown") } default: - msg = "❗ Unknown command" + msg += t.I18nBot("tgbot.commands.unknown") } t.SendAnswer(chatId, msg, isAdmin) } @@ -179,35 +232,42 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool chatId := callbackQuery.Message.Chat.ID if isAdmin { - dataArray := strings.Split(callbackQuery.Data, " ") + // get query from hash storage + decodedQuery, err := t.decodeQuery(callbackQuery.Data) + if err != nil { + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.noQuery")) + return + } + dataArray := strings.Split(decodedQuery, " ") + if len(dataArray) >= 2 && len(dataArray[1]) > 0 { email := dataArray[1] switch dataArray[0] { case "client_refresh": - t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Client refreshed successfully.", email)) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clientRefreshSuccess", "Email=="+email)) t.searchClient(chatId, email, callbackQuery.Message.MessageID) case "client_cancel": - t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("❌ %s : Operation canceled.", email)) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email)) t.searchClient(chatId, email, callbackQuery.Message.MessageID) case "ips_refresh": - t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : IPs refreshed successfully.", email)) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.IpRefreshSuccess", "Email=="+email)) t.searchClientIps(chatId, email, callbackQuery.Message.MessageID) case "ips_cancel": - t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("❌ %s : Operation canceled.", email)) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email)) t.searchClientIps(chatId, email, callbackQuery.Message.MessageID) case "tgid_refresh": - t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Client's Telegram User refreshed successfully.", email)) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.TGIdRefreshSuccess", "Email=="+email)) t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID) case "tgid_cancel": - t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("❌ %s : Operation canceled.", email)) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email)) t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID) case "reset_traffic": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("❌ Cancel Reset").WithCallbackData("client_cancel "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancelReset")).WithCallbackData(t.encodeQuery("client_cancel "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("✅ Confirm Reset Traffic?").WithCallbackData("reset_traffic_c "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmResetTraffic")).WithCallbackData(t.encodeQuery("reset_traffic_c "+email)), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) @@ -215,34 +275,34 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool err := t.inboundService.ResetClientTrafficByEmail(email) if err == nil { t.xrayService.SetToNeedRestart() - t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Traffic reset successfully.", email)) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetTrafficSuccess", "Email=="+email)) t.searchClient(chatId, email, callbackQuery.Message.MessageID) } else { - t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.") + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) } case "reset_exp": - var inlineKeyboard = tu.InlineKeyboard( + inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("❌ Cancel Reset").WithCallbackData("client_cancel "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancelReset")).WithCallbackData(t.encodeQuery("client_cancel "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("♾ Unlimited").WithCallbackData("reset_exp_c "+email+" 0"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.unlimited")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 0")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("1 Month").WithCallbackData("reset_exp_c "+email+" 30"), - tu.InlineKeyboardButton("2 Months").WithCallbackData("reset_exp_c "+email+" 60"), + 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.InlineKeyboardRow( - tu.InlineKeyboardButton("3 Months").WithCallbackData("reset_exp_c "+email+" 90"), - tu.InlineKeyboardButton("6 Months").WithCallbackData("reset_exp_c "+email+" 180"), + 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.InlineKeyboardRow( - tu.InlineKeyboardButton("9 Months").WithCallbackData("reset_exp_c "+email+" 270"), - tu.InlineKeyboardButton("12 Months").WithCallbackData("reset_exp_c "+email+" 360"), + 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.InlineKeyboardRow( - tu.InlineKeyboardButton("10 Days").WithCallbackData("reset_exp_c "+email+" 10"), - tu.InlineKeyboardButton("20 Days").WithCallbackData("reset_exp_c "+email+" 20"), + 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")), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) @@ -257,39 +317,39 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool err := t.inboundService.ResetClientExpiryTimeByEmail(email, date) if err == nil { t.xrayService.SetToNeedRestart() - t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Expire days reset successfully.", email)) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.expireResetSuccess", "Email=="+email)) t.searchClient(chatId, email, callbackQuery.Message.MessageID) return } } } - t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.") + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.searchClient(chatId, email, callbackQuery.Message.MessageID) case "ip_limit": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("❌ Cancel IP Limit").WithCallbackData("client_cancel "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancelIpLimit")).WithCallbackData(t.encodeQuery("client_cancel "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("♾ Unlimited").WithCallbackData("ip_limit_c "+email+" 0"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.unlimited")).WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 0")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("1").WithCallbackData("ip_limit_c "+email+" 1"), - tu.InlineKeyboardButton("2").WithCallbackData("ip_limit_c "+email+" 2"), + tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 1")), + tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 2")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("3").WithCallbackData("ip_limit_c "+email+" 3"), - tu.InlineKeyboardButton("4").WithCallbackData("ip_limit_c "+email+" 4"), + tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 3")), + tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 4")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("5").WithCallbackData("ip_limit_c "+email+" 5"), - tu.InlineKeyboardButton("6").WithCallbackData("ip_limit_c "+email+" 6"), - tu.InlineKeyboardButton("7").WithCallbackData("ip_limit_c "+email+" 7"), + tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 5")), + tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 6")), + tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 7")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("8").WithCallbackData("ip_limit_c "+email+" 8"), - tu.InlineKeyboardButton("9").WithCallbackData("ip_limit_c "+email+" 9"), - tu.InlineKeyboardButton("10").WithCallbackData("ip_limit_c "+email+" 10"), + tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 8")), + tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 9")), + tu.InlineKeyboardButton("10").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 10")), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) @@ -300,73 +360,73 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool err := t.inboundService.ResetClientIpLimitByEmail(email, count) if err == nil { t.xrayService.SetToNeedRestart() - t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : IP limit %d saved successfully.", email, count)) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetIpSuccess", "Email=="+email, "Count=="+strconv.Itoa(count))) t.searchClient(chatId, email, callbackQuery.Message.MessageID) return } } } - t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.") + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.searchClient(chatId, email, callbackQuery.Message.MessageID) case "clear_ips": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("❌ Cancel").WithCallbackData("ips_cancel "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("ips_cancel "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("✅ Confirm Clear IPs?").WithCallbackData("clear_ips_c "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmClearIps")).WithCallbackData(t.encodeQuery("clear_ips_c "+email)), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) case "clear_ips_c": err := t.inboundService.ClearClientIps(email) if err == nil { - t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : IPs cleared successfully.", email)) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clearIpSuccess", "Email=="+email)) t.searchClientIps(chatId, email, callbackQuery.Message.MessageID) } else { - t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.") + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) } case "ip_log": - t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Get IP Log.", email)) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.getIpLog", "Email=="+email)) t.searchClientIps(chatId, email) case "tg_user": - t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Get Telegram User Info.", email)) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.getUserInfo", "Email=="+email)) t.clientTelegramUserInfo(chatId, email) case "tgid_remove": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("❌ Cancel").WithCallbackData("tgid_cancel "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("tgid_cancel "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("✅ Confirm Remove Telegram User?").WithCallbackData("tgid_remove_c "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmRemoveTGUser")).WithCallbackData(t.encodeQuery("tgid_remove_c "+email)), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard) case "tgid_remove_c": traffic, err := t.inboundService.GetClientTrafficByEmail(email) if err != nil || traffic == nil { - t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.") + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) return } err = t.inboundService.SetClientTelegramUserID(traffic.Id, "") if err == nil { - t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Telegram User removed successfully.", email)) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.removedTGUserSuccess", "Email=="+email)) t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID) } else { - t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.") + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) } case "toggle_enable": enabled, err := t.inboundService.ToggleClientEnableByEmail(email) if err == nil { t.xrayService.SetToNeedRestart() if enabled { - t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Enabled successfully.", email)) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.enableSuccess", "Email=="+email)) } else { - t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Disabled successfully.", email)) + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.disableSuccess", "Email=="+email)) } t.searchClient(chatId, email, callbackQuery.Message.MessageID) } else { - t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.") + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) } } return @@ -389,9 +449,9 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool case "client_traffic": t.getClientUsage(chatId, callbackQuery.From.Username, strconv.FormatInt(callbackQuery.From.ID, 10)) case "client_commands": - t.SendMsgToTgbot(chatId, "To search for statistics, just use folowing command:\r\n \r\n/usage [UID|Password]\r\n \r\nUse UID for vmess/vless and Password for Trojan.") + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpClientCommands")) case "commands": - t.SendMsgToTgbot(chatId, "Search for a client email:\r\n/usage email\r\n \r\nSearch for inbounds (with client stats):\r\n/inbound [remark]") + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpAdminCommands")) } } @@ -407,49 +467,50 @@ func checkAdmin(tgId int64) bool { func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) { numericKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("Server Usage").WithCallbackData("get_usage"), - tu.InlineKeyboardButton("Get DB Backup").WithCallbackData("get_backup"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.serverUsage")).WithCallbackData(t.encodeQuery("get_usage")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.dbBackup")).WithCallbackData(t.encodeQuery("get_backup")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("Get Inbounds").WithCallbackData("inbounds"), - tu.InlineKeyboardButton("Deplete soon").WithCallbackData("deplete_soon"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.getInbounds")).WithCallbackData(t.encodeQuery("inbounds")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.depleteSoon")).WithCallbackData(t.encodeQuery("deplete_soon")), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("Commands").WithCallbackData("commands"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.commands")).WithCallbackData(t.encodeQuery("commands")), ), ) numericKeyboardClient := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("Get Usage").WithCallbackData("client_traffic"), - tu.InlineKeyboardButton("Commands").WithCallbackData("client_commands"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.clientUsage")).WithCallbackData(t.encodeQuery("client_traffic")), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.commands")).WithCallbackData(t.encodeQuery("client_commands")), ), ) - params := telego.SendMessageParams{ - ChatID: tu.ID(chatId), - Text: msg, - ParseMode: "HTML", - } + + var ReplyMarkup telego.ReplyMarkup if isAdmin { - params.ReplyMarkup = numericKeyboard + ReplyMarkup = numericKeyboard } else { - params.ReplyMarkup = numericKeyboardClient - } - _, err := bot.SendMessage(¶ms) - if err != nil { - logger.Warning("Error sending telegram message :", err) + ReplyMarkup = numericKeyboardClient } + t.SendMsgToTgbot(chatId, msg, ReplyMarkup) } func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.ReplyMarkup) { if !isRunning { return } + if msg == "" { + logger.Info("[tgbot] message is empty!") + return + } + var allMessages []string limit := 2000 + // paging message if it is big if len(msg) > limit { messages := strings.Split(msg, "\r\n \r\n") lastIndex := -1 + for _, message := range messages { if (len(allMessages) == 0) || (len(allMessages[lastIndex])+len(message) > limit) { allMessages = append(allMessages, message) @@ -487,43 +548,44 @@ func (t *Tgbot) SendMsgToTgbotAdmins(msg string) { func (t *Tgbot) SendReport() { runTime, err := t.settingService.GetTgbotRuntime() if err == nil && len(runTime) > 0 { - t.SendMsgToTgbotAdmins("🕰 Scheduled reports: " + runTime + "\r\nDate-Time: " + time.Now().Format("2006-01-02 15:04:05")) + msg := "" + msg += t.I18nBot("tgbot.messages.report", "RunTime=="+runTime) + msg += t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05")) + t.SendMsgToTgbotAdmins(msg) } + info := t.getServerUsage() t.SendMsgToTgbotAdmins(info) + exhausted := t.getExhausted() t.SendMsgToTgbotAdmins(exhausted) + backupEnable, err := t.settingService.GetTgBotBackup() if err == nil && backupEnable { - for _, adminId := range adminIds { - t.sendBackup(int64(adminId)) - } + t.SendBackupToAdmins() } } -func (t *Tgbot) SendBackUP(c *gin.Context) { +func (t *Tgbot) SendBackupToAdmins() { + if !t.IsRunning() { + return + } for _, adminId := range adminIds { t.sendBackup(int64(adminId)) } } func (t *Tgbot) getServerUsage() string { - var info string - //get hostname - name, err := os.Hostname() - if err != nil { - logger.Error("get hostname error:", err) - name = "" - } - info = fmt.Sprintf("💻 Hostname: %s\r\n", name) - info += fmt.Sprintf("🚀X-UI Version: %s\r\n", config.GetVersion()) - //get ip address - var ip string - var ipv6 string + info, ipv4, ipv6 := "", "", "" + info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) + info += t.I18nBot("tgbot.messages.version", "Version=="+config.GetVersion()) + + // get ip address netInterfaces, err := net.Interfaces() if err != nil { - logger.Error("net.Interfaces failed, err:", err.Error()) - info += "🌐 IP: Unknown\r\n \r\n" + logger.Error("net.Interfaces failed, err: ", err.Error()) + info += t.I18nBot("tgbot.messages.ip", "IP=="+t.I18nBot("tgbot.unknown")) + info += " \r\n" } else { for i := 0; i < len(netInterfaces); i++ { if (netInterfaces[i].Flags & net.FlagUp) != 0 { @@ -532,7 +594,7 @@ func (t *Tgbot) getServerUsage() string { for _, address := range addrs { if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { if ipnet.IP.To4() != nil { - ip += ipnet.IP.String() + " " + ipv4 += ipnet.IP.String() + " " } else if ipnet.IP.To16() != nil && !ipnet.IP.IsLinkLocalUnicast() { ipv6 += ipnet.IP.String() + " " } @@ -540,42 +602,44 @@ func (t *Tgbot) getServerUsage() string { } } } - info += fmt.Sprintf("🌐IP: %s\r\n🌐IPv6: %s\r\n", ip, ipv6) + + info += t.I18nBot("tgbot.messages.ipv4", "IPv4=="+ipv4) + info += t.I18nBot("tgbot.messages.ipv6", "IPv6=="+ipv6) } // get latest status of server t.lastStatus = t.serverService.GetStatus(t.lastStatus) - info += fmt.Sprintf("🔌Server Uptime: %d days\r\n", int(t.lastStatus.Uptime/86400)) - info += fmt.Sprintf("📈Server Load: %.1f, %.1f, %.1f\r\n", t.lastStatus.Loads[0], t.lastStatus.Loads[1], t.lastStatus.Loads[2]) - info += fmt.Sprintf("📋Server Memory: %s/%s\r\n", common.FormatTraffic(int64(t.lastStatus.Mem.Current)), common.FormatTraffic(int64(t.lastStatus.Mem.Total))) - info += fmt.Sprintf("🔹TcpCount: %d\r\n", t.lastStatus.TcpCount) - info += fmt.Sprintf("🔸UdpCount: %d\r\n", t.lastStatus.UdpCount) - info += fmt.Sprintf("🚦Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv))) - info += fmt.Sprintf("ℹXray status: %s", t.lastStatus.Xray.State) - + 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.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))) + info += t.I18nBot("tgbot.messages.xrayStatus", "State=="+fmt.Sprint(t.lastStatus.Xray.State)) return info } func (t *Tgbot) UserLoginNotify(username string, ip string, time string, status LoginStatus) { + if !t.IsRunning() { + return + } + if username == "" || ip == "" || time == "" { - logger.Warning("UserLoginNotify failed,invalid info") - return - } - var msg string - // Get hostname - name, err := os.Hostname() - if err != nil { - logger.Warning("get hostname error:", err) + logger.Warning("UserLoginNotify failed, invalid info!") return } + + msg := "" if status == LoginSuccess { - msg = fmt.Sprintf("✅ Successfully logged-in to the panel\r\nHostname:%s\r\n", name) + msg += t.I18nBot("tgbot.messages.loginSuccess") } else if status == LoginFail { - msg = fmt.Sprintf("❗ Login to the panel was unsuccessful\r\nHostname:%s\r\n", name) + msg += t.I18nBot("tgbot.messages.loginFailed") } - msg += fmt.Sprintf("⏰ Time:%s\r\n", time) - msg += fmt.Sprintf("🆔 Username:%s\r\n", username) - msg += fmt.Sprintf("🌐 IP:%s\r\n", ip) + + msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) + msg += t.I18nBot("tgbot.messages.username", "Username=="+username) + msg += t.I18nBot("tgbot.messages.ip", "IP=="+ip) + msg += t.I18nBot("tgbot.messages.time", "Time=="+time) t.SendMsgToTgbotAdmins(msg) } @@ -585,17 +649,19 @@ func (t *Tgbot) getInboundUsages() string { inbouds, err := t.inboundService.GetAllInbounds() if err != nil { logger.Warning("GetAllInbounds run failed:", err) - info += "❌ Failed to get inbounds" + info += t.I18nBot("tgbot.answers.getInboundsFailed") } else { // NOTE:If there no any sessions here,need to notify here // TODO:Sub-node push, automatic conversion format for _, inbound := range inbouds { - info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port) - info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down)) + info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark) + info += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)) + info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)) + if inbound.ExpiryTime == 0 { - info += "Expire date: ♾ Unlimited\r\n \r\n" + info += t.I18nBot("tgbot.messages.expire", "DateTime=="+t.I18nBot("tgbot.unlimited")) } else { - info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) + info += t.I18nBot("tgbot.messages.expire", "DateTime=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) } } } @@ -606,13 +672,14 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string, tgUserID string) traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID) if err != nil { logger.Warning(err) - msg := "❌ Something went wrong!" + msg := t.I18nBot("tgbot.wentWrong") t.SendMsgToTgbot(chatId, msg) return } + if len(traffics) == 0 { if len(tgUserName) == 0 { - msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram user id in your configuration(s).\n\nYour user id: " + tgUserID + "" + msg := t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+tgUserID) t.SendMsgToTgbot(chatId, msg) return } @@ -620,52 +687,66 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string, tgUserID string) } if err != nil { logger.Warning(err) - msg := "❌ Something went wrong!" + msg := t.I18nBot("tgbot.wentWrong") t.SendMsgToTgbot(chatId, msg) return } if len(traffics) == 0 { - msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram username or user id in your configuration(s).\n\nYour username: @" + tgUserName + "\n\nYour user id: " + tgUserID + "" + msg := t.I18nBot("tgbot.answers.askToAddUserName", "TgUserName=="+tgUserName, "TgUserID=="+tgUserID) t.SendMsgToTgbot(chatId, msg) return } + for _, traffic := range traffics { expiryTime := "" if traffic.ExpiryTime == 0 { - expiryTime = "♾Unlimited" + expiryTime = t.I18nBot("tgbot.unlimited") } else if traffic.ExpiryTime < 0 { - expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000) + expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days")) } else { expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") } + total := "" if traffic.Total == 0 { - total = "♾Unlimited" + total = t.I18nBot("tgbot.unlimited") } else { total = common.FormatTraffic((traffic.Total)) } - output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n", - traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)), - total, expiryTime) + + output := "" + output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) + output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable)) + output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) + 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.expireIn", "Time=="+expiryTime) + t.SendMsgToTgbot(chatId, output) } - t.SendAnswer(chatId, "Please choose:", false) + t.SendAnswer(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), false) } func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) { ips, err := t.inboundService.GetInboundClientIps(email) if err != nil || len(ips) == 0 { - ips = "No IP Record" + ips = t.I18nBot("tgbot.noIpRecord") } - output := fmt.Sprintf("📧 Email: %s\r\n🔢 IPs: \r\n%s\r\n", email, ips) + + output := "" + output += t.I18nBot("tgbot.messages.email", "Email=="+email) + output += t.I18nBot("tgbot.messages.ips", "IPs=="+ips) + inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("🔄 Refresh").WithCallbackData("ips_refresh "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("ips_refresh "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("❌ Clear IPs").WithCallbackData("clear_ips "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.clearIPs")).WithCallbackData(t.encodeQuery("clear_ips "+email)), ), ) + if len(messageID) > 0 { t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard) } else { @@ -677,28 +758,33 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ... traffic, client, err := t.inboundService.GetClientByEmail(email) if err != nil { logger.Warning(err) - msg := "❌ Something went wrong!" + msg := t.I18nBot("tgbot.wentWrong") t.SendMsgToTgbot(chatId, msg) return } if client == nil { - msg := "No result!" + msg := t.I18nBot("tgbot.noResult") t.SendMsgToTgbot(chatId, msg) return } - tdId := "None" + tgId := "None" if len(client.TgID) > 0 { - tdId = client.TgID + tgId = client.TgID } - output := fmt.Sprintf("📧 Email: %s\r\n👤 Telegram User: %s\r\n", email, tdId) + + output := "" + output += t.I18nBot("tgbot.messages.email", "Email=="+email) + output += t.I18nBot("tgbot.messages.TGUser", "TelegramID=="+tgId) + inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("🔄 Refresh").WithCallbackData("tgid_refresh "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("tgid_refresh "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("❌ Remove Telegram User").WithCallbackData("tgid_remove "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.removeTGUser")).WithCallbackData(t.encodeQuery("tgid_remove "+email)), ), ) + if len(messageID) > 0 { t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard) } else { @@ -709,13 +795,13 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ... } keyboard := tu.Keyboard( tu.KeyboardRow( - tu.KeyboardButton("👤 Select Telegram User").WithRequestUser(&requestUser), + tu.KeyboardButton(t.I18nBot("tgbot.buttons.selectTGUser")).WithRequestUser(&requestUser), ), tu.KeyboardRow( - tu.KeyboardButton("❌ Close Keyboard"), + tu.KeyboardButton(t.I18nBot("tgbot.buttons.closeKeyboard")), ), ).WithIsPersistent().WithResizeKeyboard() - t.SendMsgToTgbot(chatId, "👤 Select a telegram user:", keyboard) + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.buttons.selectOneTGUser"), keyboard) } } @@ -723,53 +809,63 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) { traffic, err := t.inboundService.GetClientTrafficByEmail(email) if err != nil { logger.Warning(err) - msg := "❌ Something went wrong!" + msg := t.I18nBot("tgbot.wentWrong") t.SendMsgToTgbot(chatId, msg) return } if traffic == nil { - msg := "No result!" + msg := t.I18nBot("tgbot.noResult") t.SendMsgToTgbot(chatId, msg) return } + expiryTime := "" if traffic.ExpiryTime == 0 { - expiryTime = "♾Unlimited" + expiryTime = t.I18nBot("tgbot.unlimited") } else if traffic.ExpiryTime < 0 { - expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000) + expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days")) } else { expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") } + total := "" if traffic.Total == 0 { - total = "♾Unlimited" + total = t.I18nBot("tgbot.unlimited") } else { total = common.FormatTraffic((traffic.Total)) } - output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n", - traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)), - total, expiryTime) + + output := "" + output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) + output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable)) + output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) + 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.expireIn", "Time=="+expiryTime) + inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( - tu.InlineKeyboardButton("🔄 Refresh").WithCallbackData("client_refresh "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("client_refresh "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("📈 Reset Traffic").WithCallbackData("reset_traffic "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetTraffic")).WithCallbackData(t.encodeQuery("reset_traffic "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("📅 Reset Expire Days").WithCallbackData("reset_exp "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData(t.encodeQuery("reset_exp "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("🔢 IP Log").WithCallbackData("ip_log "+email), - tu.InlineKeyboardButton("🔢 IP Limit").WithCallbackData("ip_limit "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLog")).WithCallbackData(t.encodeQuery("ip_log "+email)), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData(t.encodeQuery("ip_limit "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("👤 Set Telegram User").WithCallbackData("tg_user "+email), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.setTGUser")).WithCallbackData(t.encodeQuery("tg_user "+email)), ), tu.InlineKeyboardRow( - tu.InlineKeyboardButton("🔘 Enable / Disable").WithCallbackData("toggle_enable "+email), + 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 { @@ -781,38 +877,55 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) { inbouds, err := t.inboundService.SearchInbounds(remark) if err != nil { logger.Warning(err) - msg := "❌ Something went wrong!" + msg := t.I18nBot("tgbot.wentWrong") t.SendMsgToTgbot(chatId, msg) return } + if len(inbouds) == 0 { + msg := t.I18nBot("tgbot.noInbounds") + t.SendMsgToTgbot(chatId, msg) + return + } + for _, inbound := range inbouds { info := "" - info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port) - info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down)) + info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark) + info += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)) + info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)) + if inbound.ExpiryTime == 0 { - info += "Expire date: ♾ Unlimited\r\n \r\n" + info += t.I18nBot("tgbot.messages.expire", "DateTime=="+t.I18nBot("tgbot.unlimited")) } else { - info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) + info += t.I18nBot("tgbot.messages.expire", "DateTime=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) } t.SendMsgToTgbot(chatId, info) + for _, traffic := range inbound.ClientStats { expiryTime := "" if traffic.ExpiryTime == 0 { - expiryTime = "♾Unlimited" + expiryTime = t.I18nBot("tgbot.unlimited") } else if traffic.ExpiryTime < 0 { - expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000) + expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days")) } else { expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") } + total := "" if traffic.Total == 0 { - total = "♾Unlimited" + total = t.I18nBot("tgbot.unlimited") } else { total = common.FormatTraffic((traffic.Total)) } - output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n", - traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)), - total, expiryTime) + + output := "" + output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) + output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable)) + output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) + 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.expireIn", "Time=="+expiryTime) + t.SendMsgToTgbot(chatId, output) } } @@ -822,32 +935,41 @@ func (t *Tgbot) searchForClient(chatId int64, query string) { traffic, err := t.inboundService.SearchClientTraffic(query) if err != nil { logger.Warning(err) - msg := "❌ Something went wrong!" + msg := t.I18nBot("tgbot.wentWrong") t.SendMsgToTgbot(chatId, msg) return } if traffic == nil { - msg := "No result!" + msg := t.I18nBot("tgbot.noResult") t.SendMsgToTgbot(chatId, msg) return } + expiryTime := "" if traffic.ExpiryTime == 0 { - expiryTime = "♾Unlimited" + expiryTime = t.I18nBot("tgbot.unlimited") } else if traffic.ExpiryTime < 0 { - expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000) + expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days")) } else { expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") } + total := "" if traffic.Total == 0 { - total = "♾Unlimited" + total = t.I18nBot("tgbot.unlimited") } else { total = common.FormatTraffic((traffic.Total)) } - output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n", - traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)), - total, expiryTime) + + output := "" + output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) + output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable)) + output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) + 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.expireIn", "Time=="+expiryTime) + t.SendMsgToTgbot(chatId, output) } @@ -859,7 +981,7 @@ func (t *Tgbot) getExhausted() string { var exhaustedClients []xray.ClientTraffic var disabledInbounds []model.Inbound var disabledClients []xray.ClientTraffic - output := "" + TrafficThreshold, err := t.settingService.GetTrafficDiff() if err == nil && TrafficThreshold > 0 { trDiff = int64(TrafficThreshold) * 1073741824 @@ -872,6 +994,7 @@ func (t *Tgbot) getExhausted() string { if err != nil { logger.Warning("Unable to load Inbounds", err) } + for _, inbound := range inbounds { if inbound.Enable { if (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) || @@ -894,39 +1017,64 @@ func (t *Tgbot) getExhausted() string { disabledInbounds = append(disabledInbounds, *inbound) } } - output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds)) + + // Inbounds + output := "" + 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 += "Exhausted Inbounds:\r\n" + output += t.I18nBot("tgbot.messages.exhaustedMsg", "Type=="+t.I18nBot("tgbot.inbounds")) + for _, inbound := range exhaustedInbounds { - output += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\nTraffic: %s (↑%s,↓%s)\r\n", inbound.Remark, inbound.Port, common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down)) + output += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark) + output += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)) + output += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)) if inbound.ExpiryTime == 0 { - output += "Expire date: ♾Unlimited\r\n \r\n" + output += t.I18nBot("tgbot.messages.expire", "DateTime=="+t.I18nBot("tgbot.unlimited")) } else { - output += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) + output += t.I18nBot("tgbot.messages.expire", "DateTime=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) } + output += "\r\n \r\n" } } - output += fmt.Sprintf("Exhausted Clients count:\r\n🛑 Exhausted: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledClients), len(exhaustedClients)) + + // Clients + 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 += "Exhausted Clients:\r\n" + output += t.I18nBot("tgbot.messages.exhaustedMsg", "Type=="+t.I18nBot("tgbot.clients")) + for _, traffic := range exhaustedClients { expiryTime := "" if traffic.ExpiryTime == 0 { - expiryTime = "♾Unlimited" + expiryTime = t.I18nBot("tgbot.unlimited") } else if traffic.ExpiryTime < 0 { - expiryTime += fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000) + expiryTime += fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days")) } else { expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") } + total := "" if traffic.Total == 0 { - total = "♾Unlimited" + total = t.I18nBot("tgbot.unlimited") } else { total = common.FormatTraffic((traffic.Total)) } - output += fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire date: %s\r\n \r\n", - traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)), - total, expiryTime) + + output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) + output += t.I18nBot("tgbot.messages.active", "Enable=="+strconv.FormatBool(traffic.Enable)) + output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) + 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.expireIn", "Time=="+expiryTime) + output += "\r\n \r\n" } } @@ -934,8 +1082,9 @@ func (t *Tgbot) getExhausted() string { } func (t *Tgbot) sendBackup(chatId int64) { - sendingTime := time.Now().Format("2006-01-02 15:04:05") - t.SendMsgToTgbot(chatId, "Backup time: "+sendingTime) + output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05")) + t.SendMsgToTgbot(chatId, output) + file, err := os.Open(config.GetDBPath()) if err != nil { logger.Warning("Error in opening db file for backup: ", err) @@ -948,6 +1097,7 @@ func (t *Tgbot) sendBackup(chatId int64) { if err != nil { logger.Warning("Error in uploading backup: ", err) } + file, err = os.Open(xray.GetConfigPath()) if err != nil { logger.Warning("Error in opening config.json file for backup: ", err) @@ -997,20 +1147,3 @@ func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlin logger.Warning(err) } } - -func fromChat(u *telego.Update) *telego.Chat { - switch { - case u.Message != nil: - return &u.Message.Chat - case u.EditedMessage != nil: - return &u.EditedMessage.Chat - case u.ChannelPost != nil: - return &u.ChannelPost.Chat - case u.EditedChannelPost != nil: - return &u.EditedChannelPost.Chat - case u.CallbackQuery != nil: - return &u.CallbackQuery.Message.Chat - default: - return nil - } -} diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index 075bf6e9..e0159ee8 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -209,7 +209,7 @@ [pages.settings] "title" = "Settings" "save" = "Save" -"infoDesc" = "Every change made here needs to be saved. Please restart the panel for the changes to take effect." +"infoDesc" = "Every change made here needs to be saved. Please restart the panel to apply changes." "restartPanel" = "Restart Panel " "restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please view the panel log information on the server." "actions" = "Actions" @@ -336,6 +336,8 @@ "manualBlockedDomains" = "List of Blocked Domains" "manualDirectIPs" = "List of Direct IPs" "manualDirectDomains" = "List of Direct Domains" +"manualIPv4Domains" = "List of IPv4 Domains" +"manualWARPDomains" = "List of WARP Domains" [pages.settings.security] "admin" = "Admin" @@ -351,3 +353,115 @@ "modifyUser" = "Modify User " "originalUserPassIncorrect" = "Incorrect original username or password" "userPassMustBeNotEmpty" = "New username and new password cannot be empty" + +[tgbot] +"keyboardClosed" = "❌ Custom keyboard closed!" +"noResult" = "❗ No result!" +"noQuery" = "❌ Query not found! Please use the command again!" +"wentWrong" = "❌ Something went wrong!" +"noIpRecord" = "❗ No IP Record!" +"noInbounds" = "❗ No inbound found!" +"unlimited" = "♾ Unlimited" +"month" = "Month" +"months" = "Months" +"day" = "Day" +"days" = "Days" +"unknown" = "Unknown" +"inbounds" = "Inbounds" +"clients" = "Clients" + +[tgbot.commands] +"unknown" = "❗ Unknown command" +"pleaseChoose" = "👇 Please choose:\r\n" +"help" = "🤖 Welcome to this bot! It's designed to offer you specific data from the server, and it allows you to make modifications as needed.\r\n\r\n" +"start" = "👋 Hello {{ .Firstname }}.\r\n" +"welcome" = "🤖 Welcome to {{ .Hostname }} management bot.\r\n" +"status" = "✅ Bot is ok!" +"usage" = "❗ Please provide a text to search!" +"helpAdminCommands" = "Search for a client email:\r\n/usage [Email]\r\n \r\nSearch for inbounds (with client stats):\r\n/inbound [Remark]" +"helpClientCommands" = "To search for statistics, just use folowing command:\r\n \r\n/usage [UUID|Password]\r\n \r\nUse UUID for vmess/vless and Password for Trojan." + +[tgbot.messages] +"cpuThreshold" = "🔴 The CPU usage {{ .Percent }}% is more than threshold {{ .Threshold }}%" +"selectUserFailed" = "❌ Error in user selection!" +"userSaved" = "✅ Telegram User saved." +"loginSuccess" = "✅ Successfully logged-in to the panel.\r\n" +"loginFailed" = "❗️ Login to the panel failed.\r\n" +"report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n" +"datetime" = "⏰ Date-Time: {{ .DateTime }}\r\n" +"hostname" = "💻 Hostname: {{ .Hostname }}\r\n" +"version" = "🚀 X-UI Version: {{ .Version }}\r\n" +"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" +"ip" = "🌐 IP: {{ .IP }}\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 Memory: {{ .Current }}/{{ .Total }}\r\n" +"tcpCount" = "🔹 TcpCount: {{ .Count }}\r\n" +"udpCount" = "🔸 UdpCount: {{ .Count }}\r\n" +"traffic" = "🚦 Traffic: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ Xray Status: {{ .State }}\r\n" +"username" = "👤 Username: {{ .Username }}\r\n" +"time" = "⏰ Time: {{ .Time }}\r\n" +"inbound" = "📍 Inbound: {{ .Remark }}\r\n" +"port" = "🔌 Port: {{ .Port }}\r\n" +"expire" = "📅 Expire Date: {{ .DateTime }}\r\n \r\n" +"expireIn" = "📅 Expire In: {{ .Time }}\r\n \r\n" +"active" = "💡 Active: {{ .Enable }}\r\n" +"email" = "📧 Email: {{ .Email }}\r\n" +"upload" = "🔼 Upload↑: {{ .Upload }}\r\n" +"download" = "🔽 Download↓: {{ .Download }}\r\n" +"total" = "🔄 Total: {{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 Telegram User: {{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 Exhausted {{ .Type }}:\r\n" +"exhaustedCount" = "🚨 Exhausted {{ .Type }} count:\r\n" +"disabled" = "🛑 Disabled: {{ .Disabled }}\r\n" +"depleteSoon" = "🔜 Deplete soon: {{ .Deplete }}\r\n \r\n" +"backupTime" = "🗄 Backup Time: {{ .Time }}\r\n" +"refreshedOn" = "🔄🕒 Refreshed On: {{ .Time }}\r\n" + +[tgbot.buttons] +"closeKeyboard" = "❌ Close Keyboard" +"cancel" = "❌ Cancel" +"cancelReset" = "❌ Cancel Reset" +"cancelIpLimit" = "❌ Cancel IP Limit" +"confirmResetTraffic" = "✅ Confirm Reset Traffic?" +"confirmClearIps" = "✅ Confirm Clear IPs?" +"confirmRemoveTGUser" = "✅ Confirm Remove Telegram User?" +"dbBackup" = "Get DB Backup" +"serverUsage" = "Server Usage" +"getInbounds" = "Get Inbounds" +"depleteSoon" = "Deplete soon" +"clientUsage" = "Get Usage" +"commands" = "Commands" +"refresh" = "🔄 Refresh" +"clearIPs" = "❌ Clear IPs" +"removeTGUser" = "❌ Remove Telegram User" +"selectTGUser" = "👤 Select Telegram User" +"selectOneTGUser" = "👤 Select a telegram user:" +"resetTraffic" = "📈 Reset Traffic" +"resetExpire" = "📅 Reset Expire Days" +"ipLog" = "🔢 IP Log" +"ipLimit" = "🔢 IP Limit" +"setTGUser" = "👤 Set Telegram User" +"toggle" = "🔘 Enable / Disable" + +[tgbot.answers] +"errorOperation" = "❗ Error in Operation." +"getInboundsFailed" = "❌ Failed to get inbounds" +"canceled" = "❌ {{ .Email }} : Operation canceled." +"clientRefreshSuccess" = "✅ {{ .Email }} : Client refreshed successfully." +"IpRefreshSuccess" = "✅ {{ .Email }} : IPs refreshed successfully." +"TGIdRefreshSuccess" = "✅ {{ .Email }} : Client's Telegram User refreshed successfully." +"resetTrafficSuccess" = "✅ {{ .Email }} : Traffic reset successfully." +"expireResetSuccess" = "✅ {{ .Email }} : Expire days reset successfully." +"resetIpSuccess" = "✅ {{ .Email }} : IP limit {{ .Count }} saved successfully." +"clearIpSuccess" = "✅ {{ .Email }} : IPs cleared successfully." +"getIpLog" = "✅ {{ .Email }} : Get IP Log." +"getUserInfo" = "✅ {{ .Email }} : Get Telegram User Info." +"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: {{ .TgUserID }}" +"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: @{{ .TgUserName }}\r\n\r\nYour user id: {{ .TgUserID }}" diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml index 9e31f4ef..310f648a 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -336,6 +336,8 @@ "manualBlockedDomains" = "لیست دامنه های مسدود شده" "manualDirectIPs" = "لیست آی‌پی های مستقیم" "manualDirectDomains" = "لیست دامنه های مستقیم" +"manualIPv4Domains" = "لیست دامنه‌های IPv4" +"manualWARPDomains" = "لیست دامنه های WARP" [pages.settings.security] "admin" = "مدیر" @@ -351,3 +353,115 @@ "modifyUser" = "ویرایش کاربر" "originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد " "userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد " + +[tgbot] +"keyboardClosed" = "❌ کیبورد سفارشی بسته شد!" +"noResult" = "❗ نتیجه‌ای یافت نشد!" +"noQuery" = "❌ کوئری یافت نشد! لطفاً دستور را مجدداً استفاده کنید!" +"wentWrong" = "❌ مشکلی رخ داده است!" +"noIpRecord" = "❗ رکورد IP یافت نشد!" +"noInbounds" = "❗ هیچ ورودی یافت نشد!" +"unlimited" = "♾ نامحدود" +"month" = "ماه" +"months" = "ماه‌ها" +"day" = "روز" +"days" = "روزها" +"unknown" = "نامشخص" +"inbounds" = "ورودی‌ها" +"clients" = "کلاینت‌ها" + +[tgbot.commands] +"unknown" = "❗ دستور ناشناخته" +"pleaseChoose" = "👇 لطفاً انتخاب کنید:\r\n" +"help" = "🤖 به این ربات خوش آمدید! این ربات برای ارائه داده‌های خاص از سرور طراحی شده است و به شما امکان تغییرات لازم را می‌دهد.\r\n\r\n" +"start" = "👋 سلام {{ .Firstname }}.\r\n" +"welcome" = "🤖 به ربات مدیریت {{ .Hostname }} خوش آمدید.\r\n" +"status" = "✅ ربات در حالت عادی است!" +"usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!" +"helpAdminCommands" = "برای جستجوی ایمیل مشتری:\r\n/usage [ایمیل]\r\n \r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n/inbound [توضیح]" +"helpClientCommands" = "برای جستجوی آمار، فقط از دستور زیر استفاده کنید:\r\n \r\n/usage [UUID|رمز عبور]\r\n \r\nاز UUID برای vmess/vless و از رمز عبور برای Trojan استفاده کنید." + +[tgbot.messages] +"cpuThreshold" = "🔴 میزان استفاده از CPU {{ .Percent }}% بیشتر از آستانه {{ .Threshold }}% است." +"selectUserFailed" = "❌ خطا در انتخاب کاربر!" +"userSaved" = "✅ کاربر تلگرام ذخیره شد." +"loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n" +"loginFailed" = "❗️ ورود به پنل ناموفق بود.\r\n" +"report" = "🕰 گزارشات زمان‌بندی شده: {{ .RunTime }}\r\n" +"datetime" = "⏰ تاریخ-زمان: {{ .DateTime }}\r\n" +"hostname" = "💻 نام میزبان: {{ .Hostname }}\r\n" +"version" = "🚀 نسخه X-UI: {{ .Version }}\r\n" +"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" +"ip" = "🌐 آدرس IP: {{ .IP }}\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" +"tcpCount" = "🔹 تعداد ترافیک TCP: {{ .Count }}\r\n" +"udpCount" = "🔸 تعداد ترافیک UDP: {{ .Count }}\r\n" +"traffic" = "🚦 ترافیک: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ وضعیت Xray: {{ .State }}\r\n" +"username" = "👤 نام کاربری: {{ .Username }}\r\n" +"time" = "⏰ زمان: {{ .Time }}\r\n" +"inbound" = "📍 ورودی: {{ .Remark }}\r\n" +"port" = "🔌 پورت: {{ .Port }}\r\n" +"expire" = "📅 تاریخ انقضا: {{ .DateTime }}\r\n \r\n" +"expireIn" = "📅 باقیمانده از انقضا: {{ .Time }}\r\n \r\n" +"active" = "💡 فعال: {{ .Enable }}\r\n" +"email" = "📧 ایمیل: {{ .Email }}\r\n" +"upload" = "🔼 آپلود↑: {{ .Upload }}\r\n" +"download" = "🔽 دانلود↓: {{ .Download }}\r\n" +"total" = "🔄 کل: {{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 کاربر تلگرام: {{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 {{ .Type }} به اتمام رسیده است:\r\n" +"exhaustedCount" = "🚨 تعداد {{ .Type }} به اتمام رسیده:\r\n" +"disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n" +"depleteSoon" = "🔜 به زودی به پایان خواهد رسید: {{ .Deplete }}\r\n \r\n" +"backupTime" = "🗄 زمان پشتیبان‌گیری: {{ .Time }}\r\n" +"refreshedOn" = "🔄🕒 تازه‌سازی شده در: {{ .Time }}\r\n" + +[tgbot.buttons] +"closeKeyboard" = "❌ بستن کیبورد" +"cancel" = "❌ لغو" +"cancelReset" = "❌ لغو تنظیم مجدد" +"cancelIpLimit" = "❌ لغو محدودیت IP" +"confirmResetTraffic" = "✅ تأیید تنظیم مجدد ترافیک؟" +"confirmClearIps" = "✅ تأیید پاک‌سازی آدرس‌های IP؟" +"confirmRemoveTGUser" = "✅ تأیید حذف کاربر تلگرام؟" +"dbBackup" = "دریافت پشتیبان پایگاه داده" +"serverUsage" = "استفاده از سرور" +"getInbounds" = "دریافت ورودی‌ها" +"depleteSoon" = "به زودی به پایان خواهد رسید" +"clientUsage" = "دریافت آمار کاربر" +"commands" = "دستورات" +"refresh" = "🔄 تازه‌سازی" +"clearIPs" = "❌ پاک‌سازی آدرس‌ها" +"removeTGUser" = "❌ حذف کاربر تلگرام" +"selectTGUser" = "👤 انتخاب کاربر تلگرام" +"selectOneTGUser" = "👤 یک کاربر تلگرام را انتخاب کنید:" +"resetTraffic" = "📈 تنظیم مجدد ترافیک" +"resetExpire" = "📅 تنظیم مجدد تاریخ انقضا" +"ipLog" = "🔢 لاگ آدرس‌های IP" +"ipLimit" = "🔢 محدودیت IP" +"setTGUser" = "👤 تنظیم کاربر تلگرام" +"toggle" = "🔘 فعال / غیرفعال" + +[tgbot.answers] +"errorOperation" = "❗ خطا در عملیات." +"getInboundsFailed" = "❌ دریافت ورودی‌ها با خطا مواجه شد." +"canceled" = "❌ {{ .Email }} : عملیات لغو شد." +"clientRefreshSuccess" = "✅ {{ .Email }} : کلاینت با موفقیت تازه‌سازی شد." +"IpRefreshSuccess" = "✅ {{ .Email }} : آدرس‌ها با موفقیت تازه‌سازی شدند." +"TGIdRefreshSuccess" = "✅ {{ .Email }} : کاربر تلگرام کلاینت با موفقیت تازه‌سازی شد." +"resetTrafficSuccess" = "✅ {{ .Email }} : ترافیک با موفقیت تنظیم مجدد شد." +"expireResetSuccess" = "✅ {{ .Email }} : تاریخ انقضا با موفقیت تنظیم مجدد شد." +"resetIpSuccess" = "✅ {{ .Email }} : محدودیت آدرس IP {{ .Count }} با موفقیت ذخیره شد." +"clearIpSuccess" = "✅ {{ .Email }} : آدرس‌ها با موفقیت پاک‌سازی شدند." +"getIpLog" = "✅ {{ .Email }} : دریافت لاگ آدرس‌های IP." +"getUserInfo" = "✅ {{ .Email }} : دریافت اطلاعات کاربر تلگرام." +"removedTGUserSuccess" = "✅ {{ .Email }} : کاربر تلگرام با موفقیت حذف شد." +"enableSuccess" = "✅ {{ .Email }} : با موفقیت فعال شد." +"disableSuccess" = "✅ {{ .Email }} : با موفقیت غیرفعال شد." +"askToAddUserId" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nشناسه کاربری شما: {{ .TgUserID }}" +"askToAddUserName" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که نام کاربری یا شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nنام کاربری شما: @{{ .TgUserName }}\r\n\r\nشناسه کاربری شما: {{ .TgUserID }}" diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 15e45b84..a07ea291 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -336,6 +336,8 @@ "manualBlockedDomains" = "Список заблокированных доменов" "manualDirectIPs" = "Список прямых IP адресов" "manualDirectDomains" = "Список прямых доменов" +"manualIPv4Domains" = "Список доменов IPv4" +"manualWARPDomains" = "Список доменов WARP" [pages.settings.security] "admin" = "Админ" @@ -351,3 +353,115 @@ "modifyUser" = "Изменение пользователя" "originalUserPassIncorrect" = "Неверное имя пользователя или пароль" "userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены" + +[tgbot] +"keyboardClosed" = "❌ Закрыта настраиваемая клавиатура!" +"noResult" = "❗ Нет результатов!" +"noQuery" = "❌ Запрос не найден! Пожалуйста, повторите команду!" +"wentWrong" = "❌ Что-то пошло не так!" +"noIpRecord" = "❗ Нет записей об IP-адресе!" +"noInbounds" = "❗ Входящих соединений не найдено!" +"unlimited" = "♾ Неограниченно" +"month" = "Месяц" +"months" = "Месяцев" +"day" = "День" +"days" = "Дней" +"unknown" = "Неизвестно" +"inbounds" = "Входящие" +"clients" = "Клиенты" + +[tgbot.commands] +"unknown" = "❗ Неизвестная команда" +"pleaseChoose" = "👇 Пожалуйста, выберите:\r\n" +"help" = "🤖 Добро пожаловать в этого бота! Он предназначен для предоставления вам конкретных данных с сервера и позволяет вносить необходимые изменения.\r\n\r\n" +"start" = "👋 Привет, {{ .Firstname }}.\r\n" +"welcome" = "🤖 Добро пожаловать в бота управления {{ .Hostname }}.\r\n" +"status" = "✅ Бот работает нормально!" +"usage" = "❗ Пожалуйста, укажите текст для поиска!" +"helpAdminCommands" = "Поиск по электронной почте клиента:\r\n/usage [Email]\r\n \r\nПоиск входящих соединений (со статистикой клиента):\r\n/inbound [Remark]" +"helpClientCommands" = "Для получения статистики используйте следующую команду:\r\n \r\n/usage [UUID|Password]\r\n \r\nИспользуйте UUID для vmess/vless и пароль для Trojan." + +[tgbot.messages] +"cpuThreshold" = "🔴 Загрузка процессора составляет {{ .Percent }}%, что превышает пороговое значение {{ .Threshold }}%" +"selectUserFailed" = "❌ Ошибка при выборе пользователя!" +"userSaved" = "✅ Пользователь Telegram сохранен." +"loginSuccess" = "✅ Успешный вход в панель.\r\n" +"loginFailed" = "❗️ Ошибка входа в панель.\r\n" +"report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n" +"datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n" +"hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n" +"version" = "🚀 Версия X-UI: {{ .Version }}\r\n" +"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" +"ip" = "🌐 IP: {{ .IP }}\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" +"tcpCount" = "🔹 Количество TCP-соединений: {{ .Count }}\r\n" +"udpCount" = "🔸 Количество UDP-соединений: {{ .Count }}\r\n" +"traffic" = "🚦 Трафик: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ Состояние Xray: {{ .State }}\r\n" +"username" = "👤 Имя пользователя: {{ .Username }}\r\n" +"time" = "⏰ Время: {{ .Time }}\r\n" +"inbound" = "📍 Входящий поток: {{ .Remark }}\r\n" +"port" = "🔌 Порт: {{ .Port }}\r\n" +"expire" = "📅 Дата окончания: {{ .DateTime }}\r\n \r\n" +"expireIn" = "📅 Окончание через: {{ .Time }}\r\n \r\n" +"active" = "💡 Активен: {{ .Enable }}\r\n" +"email" = "📧 Email: {{ .Email }}\r\n" +"upload" = "🔼 Загрузка↑: {{ .Upload }}\r\n" +"download" = "🔽 Скачивание↓: {{ .Download }}\r\n" +"total" = "🔄 Всего: {{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 Пользователь Telegram: {{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 Исчерпаны {{ .Type }}:\r\n" +"exhaustedCount" = "🚨 Количество исчерпанных {{ .Type }}:\r\n" +"disabled" = "🛑 Отключено: {{ .Disabled }}\r\n" +"depleteSoon" = "🔜 Скоро исчерпание: {{ .Deplete }}\r\n \r\n" +"backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n" +"refreshedOn" = "🔄🕒 Обновлено: {{ .Time }}\r\n" + +[tgbot.buttons] +"closeKeyboard" = "❌ Закрыть клавиатуру" +"cancel" = "❌ Отмена" +"cancelReset" = "❌ Отменить сброс" +"cancelIpLimit" = "❌ Отменить лимит IP" +"confirmResetTraffic" = "✅ Подтвердить сброс трафика?" +"confirmClearIps" = "✅ Подтвердить очистку IP?" +"confirmRemoveTGUser" = "✅ Подтвердить удаление пользователя Telegram?" +"dbBackup" = "Получить резервную копию DB" +"serverUsage" = "Использование сервера" +"getInbounds" = "Получить входящие потоки" +"depleteSoon" = "Скоро исчерпание" +"clientUsage" = "Получить использование" +"commands" = "Команды" +"refresh" = "🔄 Обновить" +"clearIPs" = "❌ Очистить IP" +"removeTGUser" = "❌ Удалить пользователя Telegram" +"selectTGUser" = "👤 Выбрать пользователя Telegram" +"selectOneTGUser" = "👤 Выберите пользователя Telegram:" +"resetTraffic" = "📈 Сбросить трафик" +"resetExpire" = "📅 Сбросить дату окончания" +"ipLog" = "🔢 Лог IP" +"ipLimit" = "🔢 Лимит IP" +"setTGUser" = "👤 Установить пользователя Telegram" +"toggle" = "🔘 Вкл./Выкл." + +[tgbot.answers] +"errorOperation" = "❗ Ошибка в операции." +"getInboundsFailed" = "❌ Не удалось получить входящие потоки." +"canceled" = "❌ {{ .Email }}: Операция отменена." +"clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен." +"IpRefreshSuccess" = "✅ {{ .Email }}: IP-адреса успешно обновлены." +"TGIdRefreshSuccess" = "✅ {{ .Email }}: Пользователь Telegram клиента успешно обновлен." +"resetTrafficSuccess" = "✅ {{ .Email }}: Трафик успешно сброшен." +"expireResetSuccess" = "✅ {{ .Email }}: Дни истечения успешно сброшены." +"resetIpSuccess" = "✅ {{ .Email }}: Лимит IP ({{ .Count }}) успешно сохранен." +"clearIpSuccess" = "✅ {{ .Email }}: IP-адреса успешно очищены." +"getIpLog" = "✅ {{ .Email }}: Получен лог IP." +"getUserInfo" = "✅ {{ .Email }}: Получена информация о пользователе Telegram." +"removedTGUserSuccess" = "✅ {{ .Email }}: Пользователь Telegram успешно удален." +"enableSuccess" = "✅ {{ .Email }}: Включено успешно." +"disableSuccess" = "✅ {{ .Email }}: Отключено успешно." +"askToAddUserId" = "Ваша конфигурация не найдена!\r\nПожалуйста, попросите администратора использовать ваш идентификатор пользователя Telegram в ваших конфигурациях.\r\n\r\nВаш идентификатор пользователя: {{ .TgUserID }}" +"askToAddUserName" = "Ваша конфигурация не найдена!\r\nПожалуйста, попросите администратора использовать ваше имя пользователя или идентификатор пользователя Telegram в ваших конфигурациях.\r\n\r\nВаше имя пользователя: @{{ .TgUserName }}\r\n\r\nВаш идентификатор пользователя: {{ .TgUserID }}" diff --git a/web/translation/translate.zh_Hans.toml b/web/translation/translate.zh_Hans.toml index a1205447..fd2515da 100644 --- a/web/translation/translate.zh_Hans.toml +++ b/web/translation/translate.zh_Hans.toml @@ -336,6 +336,8 @@ "manualBlockedDomains" = "被阻止的域列表" "manualDirectIPs" = "直接 IP 列表" "manualDirectDomains" = "直接域列表" +"manualIPv4Domains" = "IPv4 域名列表" +"manualWARPDomains" = "WARP域名列表" [pages.settings.security] "admin" = "行政" @@ -351,3 +353,115 @@ "modifyUser" = "修改用户" "originalUserPassIncorrect" = "原用户名或原密码错误" "userPassMustBeNotEmpty" = "新用户名和新密码不能为空" + +[tgbot] +"keyboardClosed" = "❌ 自定义键盘已关闭!" +"noResult" = "❗ 没有结果!" +"noQuery" = "❌ 未找到查询!请重新使用命令!" +"wentWrong" = "❌ 出了点问题!" +"noIpRecord" = "❗ 没有IP记录!" +"noInbounds" = "❗ 没有找到入站连接!" +"unlimited" = "♾ 无限制" +"month" = "月" +"months" = "月" +"day" = "天" +"days" = "天" +"unknown" = "未知" +"inbounds" = "入站连接" +"clients" = "客户端" + +[tgbot.commands] +"unknown" = "❗ 未知命令" +"pleaseChoose" = "👇 请选择:\r\n" +"help" = "🤖 欢迎使用本机器人!它旨在为您提供来自服务器的特定数据,并允许您进行必要的修改。\r\n\r\n" +"start" = "👋 你好,{{ .Firstname }}。\r\n" +"welcome" = "🤖 欢迎来到{{ .Hostname }}管理机器人。\r\n" +"status" = "✅ 机器人正常运行!" +"usage" = "❗ 请输入要搜索的文本!" +"helpAdminCommands" = "搜索客户端邮箱:\r\n/usage [Email]\r\n \r\n搜索入站连接(包含客户端统计信息):\r\n/inbound [Remark]" +"helpClientCommands" = "要搜索统计信息,请使用以下命令:\r\n \r\n/usage [UUID|Password]\r\n \r\n对于vmess/vless,请使用UUID;对于Trojan,请使用密码。" + +[tgbot.messages] +"cpuThreshold" = "🔴 CPU 使用率为 {{ .Percent }}%,超过阈值 {{ .Threshold }}%" +"selectUserFailed" = "❌ 用户选择错误!" +"userSaved" = "✅ 电报用户已保存。" +"loginSuccess" = "✅ 成功登录到面板。\r\n" +"loginFailed" = "❗️ 面板登录失败。\r\n" +"report" = "🕰 定时报告:{{ .RunTime }}\r\n" +"datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n" +"hostname" = "💻 主机名:{{ .Hostname }}\r\n" +"version" = "🚀 X-UI 版本:{{ .Version }}\r\n" +"ipv6" = "🌐 IPv6:{{ .IPv6 }}\r\n" +"ipv4" = "🌐 IPv4:{{ .IPv4 }}\r\n" +"ip" = "🌐 IP:{{ .IP }}\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" +"tcpCount" = "🔹 TCP 连接数:{{ .Count }}\r\n" +"udpCount" = "🔸 UDP 连接数:{{ .Count }}\r\n" +"traffic" = "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" +"xrayStatus" = "ℹ️ Xray 状态:{{ .State }}\r\n" +"username" = "👤 用户名:{{ .Username }}\r\n" +"time" = "⏰ 时间:{{ .Time }}\r\n" +"inbound" = "📍 入站:{{ .Remark }}\r\n" +"port" = "🔌 端口:{{ .Port }}\r\n" +"expire" = "📅 过期日期:{{ .DateTime }}\r\n \r\n" +"expireIn" = "📅 剩余时间:{{ .Time }}\r\n \r\n" +"active" = "💡 激活:{{ .Enable }}\r\n" +"email" = "📧 邮箱:{{ .Email }}\r\n" +"upload" = "🔼 上传↑:{{ .Upload }}\r\n" +"download" = "🔽 下载↓:{{ .Download }}\r\n" +"total" = "🔄 总计:{{ .UpDown }} / {{ .Total }}\r\n" +"TGUser" = "👤 电报用户:{{ .TelegramID }}\r\n" +"exhaustedMsg" = "🚨 耗尽的{{ .Type }}:\r\n" +"exhaustedCount" = "🚨 耗尽的{{ .Type }}数量:\r\n" +"disabled" = "🛑 禁用:{{ .Disabled }}\r\n" +"depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n \r\n" +"backupTime" = "🗄 备份时间:{{ .Time }}\r\n" +"refreshedOn" = "🔄🕒 刷新时间:{{ .Time }}\r\n" + +[tgbot.buttons] +"closeKeyboard" = "❌ 关闭键盘" +"cancel" = "❌ 取消" +"cancelReset" = "❌ 取消重置" +"cancelIpLimit" = "❌ 取消 IP 限制" +"confirmResetTraffic" = "✅ 确认重置流量?" +"confirmClearIps" = "✅ 确认清除 IP?" +"confirmRemoveTGUser" = "✅ 确认移除 Telegram 用户?" +"dbBackup" = "获取数据库备份" +"serverUsage" = "服务器使用情况" +"getInbounds" = "获取入站信息" +"depleteSoon" = "即将耗尽" +"clientUsage" = "获取使用情况" +"commands" = "命令" +"refresh" = "🔄 刷新" +"clearIPs" = "❌ 清除 IP" +"removeTGUser" = "❌ 移除 Telegram 用户" +"selectTGUser" = "👤 选择 Telegram 用户" +"selectOneTGUser" = "👤 选择一个 Telegram 用户:" +"resetTraffic" = "📈 重置流量" +"resetExpire" = "📅 重置过期天数" +"ipLog" = "🔢 IP 日志" +"ipLimit" = "🔢 IP 限制" +"setTGUser" = "👤 设置 Telegram 用户" +"toggle" = "🔘 启用/禁用" + +[tgbot.answers] +"errorOperation" = "❗ 操作错误。" +"getInboundsFailed" = "❌ 获取入站信息失败。" +"canceled" = "❌ {{ .Email }}:操作已取消。" +"clientRefreshSuccess" = "✅ {{ .Email }}:客户端刷新成功。" +"IpRefreshSuccess" = "✅ {{ .Email }}:IP 刷新成功。" +"TGIdRefreshSuccess" = "✅ {{ .Email }}:客户端的 Telegram 用户刷新成功。" +"resetTrafficSuccess" = "✅ {{ .Email }}:流量已重置成功。" +"expireResetSuccess" = "✅ {{ .Email }}:过期天数已重置成功。" +"resetIpSuccess" = "✅ {{ .Email }}:成功保存 IP 限制数量为 {{ .Count }}。" +"clearIpSuccess" = "✅ {{ .Email }}:IP 已成功清除。" +"getIpLog" = "✅ {{ .Email }}:获取 IP 日志。" +"getUserInfo" = "✅ {{ .Email }}:获取 Telegram 用户信息。" +"removedTGUserSuccess" = "✅ {{ .Email }}:Telegram 用户已成功移除。" +"enableSuccess" = "✅ {{ .Email }}:已成功启用。" +"disableSuccess" = "✅ {{ .Email }}:已成功禁用。" +"askToAddUserId" = "未找到您的配置!\r\n请向管理员询问,在您的配置中使用您的 Telegram 用户ID。\r\n\r\n您的用户ID:{{ .TgUserID }}" +"askToAddUserName" = "未找到您的配置!\r\n请向管理员询问,在您的配置中使用您的 Telegram 用户名或用户ID。\r\n\r\n您的用户名:@{{ .TgUserName }}\r\n\r\n您的用户ID:{{ .TgUserID }}" diff --git a/web/web.go b/web/web.go index 1795e1d4..1a631d38 100644 --- a/web/web.go +++ b/web/web.go @@ -18,16 +18,14 @@ import ( "x-ui/util/common" "x-ui/web/controller" "x-ui/web/job" + "x-ui/web/locale" "x-ui/web/network" "x-ui/web/service" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" - "github.com/nicksnyder/go-i18n/v2/i18n" - "github.com/pelletier/go-toml/v2" "github.com/robfig/cron/v3" - "golang.org/x/text/language" ) //go:embed assets/* @@ -202,13 +200,23 @@ func (s *Server) initRouter() (*gin.Engine, error) { c.Header("Cache-Control", "max-age=31536000") } }) - err = s.initI18n(engine) + + // init i18n + err = locale.InitLocalizer(i18nFS, &s.settingService) if err != nil { return nil, err } + // Apply locale middleware for i18n + i18nWebFunc := func(key string, params ...string) string { + return locale.I18n(locale.Web, key, params...) + } + engine.FuncMap["i18n"] = i18nWebFunc + engine.Use(locale.LocalizerMiddleware()) + + // set static files and template if config.IsDebug() { - // for develop + // for development files, err := s.getHtmlFiles() if err != nil { return nil, err @@ -216,12 +224,12 @@ func (s *Server) initRouter() (*gin.Engine, error) { engine.LoadHTMLFiles(files...) engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets"))) } else { - // for prod - t, err := s.getHtmlTemplate(engine.FuncMap) + // for production + template, err := s.getHtmlTemplate(engine.FuncMap) if err != nil { return nil, err } - engine.SetHTMLTemplate(t) + engine.SetHTMLTemplate(template) engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS})) } @@ -239,87 +247,6 @@ func (s *Server) initRouter() (*gin.Engine, error) { return engine, nil } -func (s *Server) initI18n(engine *gin.Engine) error { - bundle := i18n.NewBundle(language.SimplifiedChinese) - bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) - err := fs.WalkDir(i18nFS, "translation", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() { - return nil - } - data, err := i18nFS.ReadFile(path) - if err != nil { - return err - } - _, err = bundle.ParseMessageFileBytes(data, path) - return err - }) - if err != nil { - return err - } - - findI18nParamNames := func(key string) []string { - names := make([]string, 0) - keyLen := len(key) - for i := 0; i < keyLen-1; i++ { - if key[i:i+2] == "{{" { // 判断开头 "{{" - j := i + 2 - isFind := false - for ; j < keyLen-1; j++ { - if key[j:j+2] == "}}" { // 结尾 "}}" - isFind = true - break - } - } - if isFind { - names = append(names, key[i+3:j]) - } - } - } - return names - } - - var localizer *i18n.Localizer - - I18n := func(key string, params ...string) (string, error) { - names := findI18nParamNames(key) - if len(names) != len(params) { - return "", common.NewError("find names:", names, "---------- params:", params, "---------- num not equal") - } - templateData := map[string]interface{}{} - for i := range names { - templateData[names[i]] = params[i] - } - return localizer.Localize(&i18n.LocalizeConfig{ - MessageID: key, - TemplateData: templateData, - }) - } - - engine.FuncMap["i18n"] = I18n - - engine.Use(func(c *gin.Context) { - //accept := c.GetHeader("Accept-Language") - - var lang string - - if cookie, err := c.Request.Cookie("lang"); err == nil { - lang = cookie.Value - } else { - lang = c.GetHeader("Accept-Language") - } - - localizer = i18n.NewLocalizer(bundle, lang) - c.Set("localizer", localizer) - c.Set("I18n", I18n) - c.Next() - }) - - return nil -} - func (s *Server) startTask() { err := s.xrayService.RestartXray(true) if err != nil { @@ -346,7 +273,7 @@ func (s *Server) startTask() { if (err == nil) && (isTgbotenabled) { runtime, err := s.settingService.GetTgbotRuntime() if err != nil || runtime == "" { - logger.Errorf("Add NewStatsNotifyJob error[%s],Runtime[%s] invalid,wil run default", err, runtime) + logger.Errorf("Add NewStatsNotifyJob error[%s], Runtime[%s] invalid, will run default", err, runtime) runtime = "@daily" } logger.Infof("Tg notify enabled,run at %s", runtime) @@ -356,12 +283,14 @@ func (s *Server) startTask() { return } + // check for Telegram bot callback query hash storage reset + s.cron.AddJob("@every 2m", job.NewCheckHashStorageJob()) + // Check CPU load and alarm to TgBot if threshold passes cpuThreshold, err := s.settingService.GetTgCpu() if (err == nil) && (cpuThreshold > 0) { s.cron.AddJob("@every 10s", job.NewCheckCpuJob()) } - } else { s.cron.Remove(entry) } @@ -441,7 +370,7 @@ func (s *Server) Start() (err error) { isTgbotenabled, err := s.settingService.GetTgbotenabled() if (err == nil) && (isTgbotenabled) { tgBot := s.tgbotService.NewTgbot() - tgBot.Start() + tgBot.Start(i18nFS) } return nil @@ -453,7 +382,7 @@ func (s *Server) Stop() error { if s.cron != nil { s.cron.Stop() } - if s.tgbotService.IsRunnging() { + if s.tgbotService.IsRunning() { s.tgbotService.Stop() } var err1 error