From 9c170fda263c063c657e7971faa5ec535be2efba Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:07:02 +0430 Subject: [PATCH 01/20] update settings ui --- web/assets/css/custom.css | 6 +++- web/html/xui/inbounds.html | 4 +-- web/html/xui/settings.html | 68 ++++++++++++++++++++++++++------------ 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/web/assets/css/custom.css b/web/assets/css/custom.css index 9ea89918..67c67708 100644 --- a/web/assets/css/custom.css +++ b/web/assets/css/custom.css @@ -197,7 +197,11 @@ body { .ant-layout-sider-zero-width-trigger, .ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu, .ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu { - background:#161b22 + background:#1a212a +} + +.ant-tabs:not(.ant-card-dark) { + background-color: white; } .ant-card-dark { diff --git a/web/html/xui/inbounds.html b/web/html/xui/inbounds.html index 83ae59ad..ef33c7db 100644 --- a/web/html/xui/inbounds.html +++ b/web/html/xui/inbounds.html @@ -105,8 +105,8 @@ + checked-children='{{ i18n "search" }}' un-checked-children='{{ i18n "filter" }}' + @change="toggleFilter" style="margin-right: 10px;"> diff --git a/web/html/xui/settings.html b/web/html/xui/settings.html index 6fdc6763..f1da86d1 100644 --- a/web/html/xui/settings.html +++ b/web/html/xui/settings.html @@ -19,6 +19,32 @@ .ant-list-item { display: block; } + + .alert-msg { + color: inherit; + font-weight: bold; + font-size: 18px; + padding: 20px 20px; + text-align: center; + } + + .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; + } @@ -31,11 +57,11 @@ {{ i18n "pages.settings.save" }} {{ i18n "pages.settings.restartPanel" }} - + -

- +

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

@@ -97,8 +123,8 @@ -

- +

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

@@ -108,8 +134,8 @@ -

- +

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

@@ -153,8 +179,8 @@
-

- +

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

@@ -165,8 +191,8 @@
-

- +

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

@@ -179,8 +205,8 @@
-

- +

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

@@ -193,8 +219,8 @@
-

- +

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

@@ -203,8 +229,8 @@
-

- +

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

@@ -241,8 +267,8 @@
-

- +

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

@@ -257,8 +283,8 @@
-

- +

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

From 15fdb5843313b9efb21fc8ac636ef29cac532e4b Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:08:03 +0430 Subject: [PATCH 02/20] FIX redirect after restart panel --- web/html/xui/settings.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/html/xui/settings.html b/web/html/xui/settings.html index f1da86d1..b786639c 100644 --- a/web/html/xui/settings.html +++ b/web/html/xui/settings.html @@ -426,7 +426,12 @@ if (msg.success) { this.loading(true); await PromiseUtil.sleep(5000); - window.location.replace(this.allSetting.webBasePath + "xui/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 resetXrayConfigToDefault() { From 0c189e50ec1e5a61a1474dc42cbfaa0691b8b503 Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:12:57 +0430 Subject: [PATCH 03/20] Add manual list for ipv4 and fixed it --- web/html/xui/settings.html | 54 +++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/web/html/xui/settings.html b/web/html/xui/settings.html index b786639c..0a0dd83b 100644 --- a/web/html/xui/settings.html +++ b/web/html/xui/settings.html @@ -238,6 +238,7 @@ + @@ -522,30 +523,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: { @@ -620,6 +621,15 @@ 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); + } + }, manualBlockedIPs: { get: function () { return JSON.stringify(this.blockedIPs, null, 2); }, set: debounce(function (value) { this.blockedIPs = JSON.parse(value); }, 1000) @@ -636,6 +646,10 @@ 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) + }, torrentSettings: { get: function () { return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols); @@ -689,40 +703,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: { From d31feba2c8b9c580b212ad140246f11ec9341d4a Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:13:21 +0430 Subject: [PATCH 04/20] Update .gitignore --- .gitignore | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 From f2394844ffe88006856219682ae032a6030c2382 Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:15:09 +0430 Subject: [PATCH 05/20] Update translations --- web/translation/translate.en_US.toml | 1 + web/translation/translate.fa_IR.toml | 1 + web/translation/translate.ru_RU.toml | 1 + web/translation/translate.zh_Hans.toml | 1 + 4 files changed, 4 insertions(+) diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index 073cf0ab..b536e90b 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -332,6 +332,7 @@ "manualBlockedDomains" = "List of Blocked Domains" "manualDirectIPs" = "List of Direct IPs" "manualDirectDomains" = "List of Direct Domains" +"manualIPv4Domains" = "List of IPv4 Domains" [pages.settings.toasts] "modifySettings" = "Modify Settings " diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml index 3249b564..57104b08 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -330,6 +330,7 @@ "manualBlockedDomains" = "لیست دامنه های مسدود شده" "manualDirectIPs" = "لیست آی‌پی های مستقیم" "manualDirectDomains" = "لیست دامنه های مستقیم" +"manualIPv4Domains" = "لیست دامنه‌های IPv4" [pages.settings.toasts] "modifySettings" = "ویرایش تنظیمات" diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 1b7a8f07..6221c07f 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -331,6 +331,7 @@ "manualBlockedDomains" = "Список заблокированных доменов" "manualDirectIPs" = "Список прямых IP-адресов" "manualDirectDomains" = "Список прямых доменов" +"manualIPv4Domains" = "Список доменов IPv4" [pages.settings.toasts] "modifySettings" = "Изменение настроек" diff --git a/web/translation/translate.zh_Hans.toml b/web/translation/translate.zh_Hans.toml index 50654959..89847ffd 100644 --- a/web/translation/translate.zh_Hans.toml +++ b/web/translation/translate.zh_Hans.toml @@ -331,6 +331,7 @@ "manualBlockedDomains" = "被阻止的域列表" "manualDirectIPs" = "直接 IP 列表" "manualDirectDomains" = "直接域列表" +"manualIPv4Domains" = "IPv4 域名列表" [pages.settings.toasts] "modifySettings" = "修改设置" From 60eb61bd600eae89ad877f98af779951d3c0ffd6 Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:17:25 +0430 Subject: [PATCH 06/20] Add tgLang option --- web/assets/js/model/models.js | 1 + web/entity/entity.go | 1 + web/network/{autp_https_conn.go => auto_https_conn.go} | 0 web/service/setting.go | 1 + 4 files changed, 3 insertions(+) rename web/network/{autp_https_conn.go => auto_https_conn.go} (100%) diff --git a/web/assets/js/model/models.js b/web/assets/js/model/models.js index 21bd5567..b6fb49c9 100644 --- a/web/assets/js/model/models.js +++ b/web/assets/js/model/models.js @@ -179,6 +179,7 @@ class AllSetting { this.tgRunTime = "@daily"; this.tgBotBackup = false; this.tgCpu = ""; + this.tgLang = ""; this.xrayTemplateConfig = ""; this.subEnable = false; this.subListen = ""; diff --git a/web/entity/entity.go b/web/entity/entity.go index cfdd3b6b..c9a90911 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"` SubEnable bool `json:"subEnable" form:"subEnable"` 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 638d9107..05a54f9e 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", "subEnable": "false", "subListen": "", "subPort": "2096", From 2990f71fe236ef898dbaa92e64c3912cad25690c Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:18:21 +0430 Subject: [PATCH 07/20] Add tgLang option to settings --- web/html/xui/settings.html | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/web/html/xui/settings.html b/web/html/xui/settings.html index 0a0dd83b..51db87c6 100644 --- a/web/html/xui/settings.html +++ b/web/html/xui/settings.html @@ -280,6 +280,29 @@ + + + + + + + + + + +
From afeab0975341a907ce67eef34ac8cebccfa7a183 Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:29:35 +0430 Subject: [PATCH 08/20] some fix for tgbot --- web/controller/index.go | 5 +-- web/service/tgbot.go | 73 +++++++++++++++++++++++++++++++---------- web/web.go | 2 +- 3 files changed, 59 insertions(+), 21 deletions(-) diff --git a/web/controller/index.go b/web/controller/index.go index a5c49e71..5cc14b06 100644 --- a/web/controller/index.go +++ b/web/controller/index.go @@ -58,15 +58,16 @@ func (a *IndexController) login(c *gin.Context) { pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyPassword")) return } + user := a.userService.CheckUser(form.Username, form.Password) 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) + a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0) pureJsonMsg(c, false, I18n(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) } diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 34c4550e..7fbed373 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -77,7 +77,7 @@ func (t *Tgbot) Start() error { return nil } -func (t *Tgbot) IsRunnging() bool { +func (t *Tgbot) IsRunning() bool { return isRunning } @@ -202,29 +202,32 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) { tgbotapi.NewInlineKeyboardButtonData("Commands", "client_commands"), ), ) - msgConfig := tgbotapi.NewMessage(chatId, msg) - msgConfig.ParseMode = "HTML" + var keyboardMarkup tgbotapi.InlineKeyboardMarkup if isAdmin { - msgConfig.ReplyMarkup = numericKeyboard + keyboardMarkup = numericKeyboard } else { - msgConfig.ReplyMarkup = numericKeyboardClient - } - _, err := bot.Send(msgConfig) - if err != nil { - logger.Warning("Error sending telegram message :", err) + keyboardMarkup = numericKeyboardClient } + t.SendMsgToTgbot(chatId, msg, keyboardMarkup) } -func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) { +func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string, replyMarkup ...tgbotapi.InlineKeyboardMarkup) { 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) @@ -239,6 +242,9 @@ func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) { for _, message := range allMessages { info := tgbotapi.NewMessage(tgid, message) info.ParseMode = "HTML" + if len(replyMarkup) > 0 { + info.ReplyMarkup = replyMarkup[0] + } _, err := bot.Send(info) if err != nil { logger.Warning("Error sending telegram message :", err) @@ -273,12 +279,12 @@ func (t *Tgbot) SendReport() { func (t *Tgbot) getServerUsage() string { var info string //get hostname - name, err := os.Hostname() + hostname, err := os.Hostname() if err != nil { logger.Error("get hostname error:", err) - name = "" + hostname = "" } - info = fmt.Sprintf("💻 Hostname: %s\r\n", name) + info = fmt.Sprintf("💻 Hostname: %s\r\n", hostname) info += fmt.Sprintf("🚀X-UI Version: %s\r\n", config.GetVersion()) //get ip address var ip string @@ -320,25 +326,32 @@ func (t *Tgbot) getServerUsage() string { } 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() + hostname, err := os.Hostname() if err != nil { logger.Warning("get hostname error:", err) return } + + msg := "" if status == LoginSuccess { - msg = fmt.Sprintf("✅ Successfully logged-in to the panel\r\nHostname:%s\r\n", name) + msg = fmt.Sprintf("✅ Successfully logged-in to the panel\r\nHostname:%s\r\n", hostname) } else if status == LoginFail { - msg = fmt.Sprintf("❗ Login to the panel was unsuccessful\r\nHostname:%s\r\n", name) + msg = fmt.Sprintf("❗ Login to the panel was unsuccessful\r\nHostname:%s\r\n", hostname) } msg += fmt.Sprintf("⏰ Time:%s\r\n", time) msg += fmt.Sprintf("🆔 Username:%s\r\n", username) msg += fmt.Sprintf("🌐 IP:%s\r\n", ip) + t.SendMsgToTgbotAdmins(msg) } @@ -419,6 +432,7 @@ func (t *Tgbot) searchClient(chatId int64, email string) { t.SendMsgToTgbot(chatId, msg) return } + expiryTime := "" if traffic.ExpiryTime == 0 { expiryTime = "♾Unlimited" @@ -433,9 +447,11 @@ func (t *Tgbot) searchClient(chatId int64, email string) { } 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) + t.SendMsgToTgbot(chatId, output) } @@ -447,6 +463,13 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) { t.SendMsgToTgbot(chatId, msg) return } + + if len(inbouds) == 0 { + msg := "❌ No inbounds found!" + t.SendMsgToTgbot(chatId, msg) + return + } + for _, inbound := range inbouds { info := "" info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port) @@ -457,6 +480,7 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) { info += fmt.Sprintf("Expire date:%s\r\n \r\n", 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 { @@ -472,6 +496,7 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) { } 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) @@ -493,6 +518,7 @@ func (t *Tgbot) searchForClient(chatId int64, query string) { t.SendMsgToTgbot(chatId, msg) return } + expiryTime := "" if traffic.ExpiryTime == 0 { expiryTime = "♾Unlimited" @@ -501,15 +527,18 @@ func (t *Tgbot) searchForClient(chatId int64, query string) { } else { expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") } + total := "" if traffic.Total == 0 { total = "♾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) + t.SendMsgToTgbot(chatId, output) } @@ -521,7 +550,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 @@ -534,6 +563,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)) || @@ -556,6 +586,8 @@ func (t *Tgbot) getExhausted() string { disabledInbounds = append(disabledInbounds, *inbound) } } + + output := "" output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds)) if len(exhaustedInbounds) > 0 { output += "Exhausted Inbounds:\r\n" @@ -568,6 +600,7 @@ func (t *Tgbot) getExhausted() string { } } } + output += fmt.Sprintf("Exhausted Clients count:\r\n🛑 Exhausted: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledClients), len(exhaustedClients)) if len(exhaustedClients) > 0 { output += "Exhausted Clients:\r\n" @@ -596,6 +629,10 @@ func (t *Tgbot) getExhausted() string { } func (t *Tgbot) sendBackup(chatId int64) { + if !t.IsRunning() { + return + } + sendingTime := time.Now().Format("2006-01-02 15:04:05") t.SendMsgToTgbot(chatId, "Backup time: "+sendingTime) file := tgbotapi.FilePath(config.GetDBPath()) diff --git a/web/web.go b/web/web.go index 7d6cd926..db1047c0 100644 --- a/web/web.go +++ b/web/web.go @@ -421,7 +421,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 From c0e670a5b6e08dacb761856c613c0fa1cb05a147 Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:32:52 +0430 Subject: [PATCH 09/20] Refactor i18n localizer --- web/locale/locale.go | 110 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 web/locale/locale.go diff --git a/web/locale/locale.go b/web/locale/locale.go new file mode 100644 index 00000000..ba7a30e5 --- /dev/null +++ b/web/locale/locale.go @@ -0,0 +1,110 @@ +package locale + +import ( + "embed" + "io/fs" + "strings" + "x-ui/logger" + + "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 + } + + 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 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 +} From f77cec681a2388ae14ce8202f357b3eaf1c589f5 Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:37:01 +0430 Subject: [PATCH 10/20] replace new localizer to web.go --- web/service/setting.go | 4 ++ web/web.go | 99 +++++------------------------------------- 2 files changed, 14 insertions(+), 89 deletions(-) diff --git a/web/service/setting.go b/web/service/setting.go index 05a54f9e..510944b0 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -249,6 +249,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/web.go b/web/web.go index db1047c0..a77a4b6a 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/* @@ -179,13 +177,16 @@ 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 } + // set static files and template if config.IsDebug() { - // for develop + // for development files, err := s.getHtmlFiles() if err != nil { return nil, err @@ -193,12 +194,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})) } @@ -212,85 +213,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) { - 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 { @@ -314,7 +236,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) @@ -329,7 +251,6 @@ func (s *Server) startTask() { if (err == nil) && (cpuThreshold > 0) { s.cron.AddJob("@every 10s", job.NewCheckCpuJob()) } - } else { s.cron.Remove(entry) } From 068decfb5e6cacb652ce0f2a17d8b745f8740576 Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:38:02 +0430 Subject: [PATCH 11/20] add localizer middleware to web.go --- web/web.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/web.go b/web/web.go index a77a4b6a..ba3d04cc 100644 --- a/web/web.go +++ b/web/web.go @@ -184,6 +184,13 @@ func (s *Server) initRouter() (*gin.Engine, error) { return nil, err } + // Apply locale middleware for i18n + webI18nFunc := func(key string, params ...string) string { + return locale.I18n(locale.Web, key, params...) + } + engine.FuncMap["i18n"] = webI18nFunc + engine.Use(locale.LocalizerMiddleware()) + // set static files and template if config.IsDebug() { // for development From dd363d2ead6706e1636288538e1deea31f372794 Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:38:42 +0430 Subject: [PATCH 12/20] create LocalizerMiddleware func --- web/locale/locale.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/web/locale/locale.go b/web/locale/locale.go index ba7a30e5..8b32d04d 100644 --- a/web/locale/locale.go +++ b/web/locale/locale.go @@ -6,6 +6,7 @@ import ( "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" @@ -82,6 +83,24 @@ func I18n(i18nType I18nType, key string, params ...string) string { return msg } +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 { From 43dc1e5522d2ce139f9042a99d85d9fb102a308d Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:40:40 +0430 Subject: [PATCH 13/20] update I18n function for controller --- web/controller/base.go | 23 ++++++++++++++--------- web/web.go | 4 ++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/web/controller/base.go b/web/controller/base.go index a9659bd2..674a195d 100644 --- a/web/controller/base.go +++ b/web/controller/base.go @@ -1,9 +1,12 @@ package controller import ( - "github.com/gin-gonic/gin" "net/http" + "x-ui/logger" + "x-ui/web/locale" "x-ui/web/session" + + "github.com/gin-gonic/gin" ) type BaseController struct { @@ -12,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")) } @@ -22,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/web.go b/web/web.go index ba3d04cc..f1e7dcde 100644 --- a/web/web.go +++ b/web/web.go @@ -185,10 +185,10 @@ func (s *Server) initRouter() (*gin.Engine, error) { } // Apply locale middleware for i18n - webI18nFunc := func(key string, params ...string) string { + i18nWebFunc := func(key string, params ...string) string { return locale.I18n(locale.Web, key, params...) } - engine.FuncMap["i18n"] = webI18nFunc + engine.FuncMap["i18n"] = i18nWebFunc engine.Use(locale.LocalizerMiddleware()) // set static files and template From b5f16a476bb0bfab5a3c07f332aaaec9dd5d21c4 Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:42:05 +0430 Subject: [PATCH 14/20] update controllers to use I18nWeb func --- web/controller/inbound.go | 32 +++++++++++++++---------------- web/controller/index.go | 10 +++++----- web/controller/server.go | 4 ++-- web/controller/setting.go | 40 +++++++++++++++++++-------------------- web/controller/util.go | 6 +++--- 5 files changed, 46 insertions(+), 46 deletions(-) diff --git a/web/controller/inbound.go b/web/controller/inbound.go index f84a5721..8c1b993c 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -58,7 +58,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) @@ -66,12 +66,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) @@ -90,7 +90,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) @@ -98,7 +98,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() } @@ -107,11 +107,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() } @@ -120,7 +120,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{ @@ -128,11 +128,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() } @@ -142,7 +142,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 } @@ -160,7 +160,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") @@ -182,7 +182,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 } @@ -200,7 +200,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") @@ -228,7 +228,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 } @@ -243,7 +243,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 5cc14b06..e6d7dc92 100644 --- a/web/controller/index.go +++ b/web/controller/index.go @@ -47,15 +47,15 @@ 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 } @@ -64,7 +64,7 @@ func (a *IndexController) login(c *gin.Context) { if user == nil { logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password) a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0) - pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword")) + pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword")) return } else { logger.Infof("%s login success ,Ip Address: %s\n", form.Username, getRemoteIp(c)) @@ -85,7 +85,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 7122d0ce..58587ff5 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 305c5222..643f9723 100644 --- a/web/controller/setting.go +++ b/web/controller/setting.go @@ -43,7 +43,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) @@ -52,57 +52,57 @@ func (a *SettingController) getAllSetting(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 } tgBotEnable, err := a.settingService.GetTgbotenabled() if err != nil { - jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) return } subEnable, err := a.settingService.GetSubEnable() if err != nil { - jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) return } subPort, err := a.settingService.GetSubPort() if err != nil { - jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) return } subPath, err := a.settingService.GetSubPath() if err != nil { - jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) return } subDomain, err := a.settingService.GetSubDomain() if err != nil { - jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) return } subKeyFile, err := a.settingService.GetSubKeyFile() if err != nil { - jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) return } subCertFile, err := a.settingService.GetSubCertFile() if err != nil { - jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err) + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) return } subTLS := false @@ -128,27 +128,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) @@ -157,18 +157,18 @@ 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) getDefaultXrayConfig(c *gin.Context) { defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig() 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) 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) } From a34496910d1d676131ce77a169d42dcbab64e79a Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:43:37 +0430 Subject: [PATCH 15/20] add tgBot localizer --- web/locale/locale.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/web/locale/locale.go b/web/locale/locale.go index 8b32d04d..9a4357c3 100644 --- a/web/locale/locale.go +++ b/web/locale/locale.go @@ -37,6 +37,11 @@ func InitLocalizer(i18nFS embed.FS, settingService SettingService) error { return err } + // setup bot locale + if err := initTGBotLocalizer(settingService); err != nil { + return err + } + return nil } @@ -83,6 +88,16 @@ func I18n(i18nType I18nType, key string, params ...string) string { 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 From 61d11ac66ab17e5e0ccf2c685707d71911a21a15 Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:45:39 +0430 Subject: [PATCH 16/20] init i18n in tgbot --- web/service/tgbot.go | 13 ++++++++++++- web/web.go | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 7fbed373..24585c99 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -1,6 +1,7 @@ package service import ( + "embed" "fmt" "net" "os" @@ -11,6 +12,7 @@ import ( "x-ui/database/model" "x-ui/logger" "x-ui/util/common" + "x-ui/web/locale" "x-ui/xray" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" @@ -38,7 +40,16 @@ 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) Start(i18nFS embed.FS) error { + err := locale.InitLocalizer(i18nFS, &t.settingService) + if err != nil { + return err + } + tgBottoken, err := t.settingService.GetTgBotToken() if err != nil || tgBottoken == "" { logger.Warning("Get TgBotToken failed:", err) diff --git a/web/web.go b/web/web.go index f1e7dcde..a8c9a533 100644 --- a/web/web.go +++ b/web/web.go @@ -337,7 +337,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 From 4b3abe9cdfb7af7215f3c83b3f7dc5d08f53143d Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:51:54 +0430 Subject: [PATCH 17/20] add createBackup api --- web/controller/api.go | 24 +++++++++++++++++++++++- web/service/tgbot.go | 13 ++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/web/controller/api.go b/web/controller/api.go index 2134a792..73a291bf 100644 --- a/web/controller/api.go +++ b/web/controller/api.go @@ -1,10 +1,15 @@ package controller -import "github.com/gin-gonic/gin" +import ( + "x-ui/web/service" + + "github.com/gin-gonic/gin" +) type APIController struct { BaseController inboundController *InboundController + Tgbot service.Tgbot } func NewAPIController(g *gin.RouterGroup) *APIController { @@ -30,6 +35,7 @@ func (a *APIController) initRouter(g *gin.RouterGroup) { g.POST("/resetAllTraffics", a.resetAllTraffics) g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics) g.POST("/delDepletedClients/:id", a.delDepletedClients) + g.GET("/createbackup", a.createBackup) a.inboundController = NewInboundController(g) } @@ -37,39 +43,55 @@ func (a *APIController) initRouter(g *gin.RouterGroup) { func (a *APIController) inbounds(c *gin.Context) { a.inboundController.getInbounds(c) } + func (a *APIController) inbound(c *gin.Context) { a.inboundController.getInbound(c) } + func (a *APIController) getClientTraffics(c *gin.Context) { a.inboundController.getClientTraffics(c) } + func (a *APIController) addInbound(c *gin.Context) { a.inboundController.addInbound(c) } + func (a *APIController) delInbound(c *gin.Context) { a.inboundController.delInbound(c) } + func (a *APIController) updateInbound(c *gin.Context) { a.inboundController.updateInbound(c) } + func (a *APIController) addInboundClient(c *gin.Context) { a.inboundController.addInboundClient(c) } + func (a *APIController) delInboundClient(c *gin.Context) { a.inboundController.delInboundClient(c) } + func (a *APIController) updateInboundClient(c *gin.Context) { a.inboundController.updateInboundClient(c) } + func (a *APIController) resetClientTraffic(c *gin.Context) { a.inboundController.resetClientTraffic(c) } + func (a *APIController) resetAllTraffics(c *gin.Context) { a.inboundController.resetAllTraffics(c) } + func (a *APIController) resetAllClientTraffics(c *gin.Context) { a.inboundController.resetAllClientTraffics(c) } + func (a *APIController) delDepletedClients(c *gin.Context) { a.inboundController.delDepletedClients(c) } + +func (a *APIController) createBackup(c *gin.Context) { + a.Tgbot.SendBackupToAdmins() +} diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 24585c99..b036b296 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -281,9 +281,16 @@ func (t *Tgbot) SendReport() { 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) SendBackupToAdmins() { + if !t.IsRunning() { + return + } + for _, adminId := range adminIds { + t.sendBackup(int64(adminId)) } } From e4300badc51a8046887c7237187a173cbcb0339b Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 06:52:07 +0430 Subject: [PATCH 18/20] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c33eda77..bae38b2c 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ docker build -t x-ui . | :----: | ------------------------------- | ----------------------------------------- | | `GET` | `"/"` | Get all inbounds | | `GET` | `"/get/:id"` | Get inbound with inbound.id | +| `GET` | `"/createbackup"` | Telegram bot sends backup to admins | | `POST` | `"/add"` | Add inbound | | `POST` | `"/del/:id"` | Delete Inbound | | `POST` | `"/update/:id"` | Update Inbound | From a241a97ef601ec0ff989942361a4cee57c90cd11 Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 07:43:20 +0430 Subject: [PATCH 19/20] Update tgbot locale + add I18nBot --- web/job/check_cpu_usage.go | 7 +- web/service/tgbot.go | 288 ++++++++++++++++++++++--------------- 2 files changed, 179 insertions(+), 116 deletions(-) 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/service/tgbot.go b/web/service/tgbot.go index b036b296..7bfdcc48 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -21,6 +21,7 @@ import ( var bot *tgbotapi.BotAPI var adminIds []int64 var isRunning bool +var hostname string type LoginStatus byte @@ -50,6 +51,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error { return err } + t.SetHostname() tgBottoken, err := t.settingService.GetTgBotToken() if err != nil || tgBottoken == "" { logger.Warning("Get TgBotToken failed:", err) @@ -92,6 +94,16 @@ 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() { bot.StopReceivingUpdates() logger.Info("Stop Telegram receiver ...") @@ -126,16 +138,16 @@ func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin b // Extract the command from the Message. switch message.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(message.CommandArguments()) > 1 { if isAdmin { @@ -144,16 +156,16 @@ func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin b t.searchForClient(chatId, message.CommandArguments()) } } else { - msg = "❗Please provide a text for search!" + msg += t.I18nBot("tgbot.commands.usage") } case "inbound": if isAdmin { t.searchInbound(chatId, message.CommandArguments()) } 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) } @@ -178,9 +190,9 @@ func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bo case "client_traffic": t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName) case "client_commands": - t.SendMsgToTgbot(callbackQuery.From.ID, "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(callbackQuery.From.ID, t.I18nBot("tgbot.commands.helpClientCommands")) case "commands": - t.SendMsgToTgbot(callbackQuery.From.ID, "Search for a client email:\r\n/usage email\r\n \r\nSearch for inbounds (with client stats):\r\n/inbound [remark]") + t.SendMsgToTgbot(callbackQuery.From.ID, t.I18nBot("tgbot.commands.helpAdminCommands")) } } @@ -194,25 +206,26 @@ func checkAdmin(tgId int64) bool { } func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) { - var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup( + numericKeyboard := tgbotapi.NewInlineKeyboardMarkup( tgbotapi.NewInlineKeyboardRow( - tgbotapi.NewInlineKeyboardButtonData("Server Usage", "get_usage"), - tgbotapi.NewInlineKeyboardButtonData("Get DB Backup", "get_backup"), + tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.serverUsage"), "get_usage"), + tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.dbBackup"), "get_backup"), ), tgbotapi.NewInlineKeyboardRow( - tgbotapi.NewInlineKeyboardButtonData("Get Inbounds", "inbounds"), - tgbotapi.NewInlineKeyboardButtonData("Deplete soon", "deplete_soon"), + tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.getInbounds"), "inbounds"), + tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.depleteSoon"), "deplete_soon"), ), tgbotapi.NewInlineKeyboardRow( - tgbotapi.NewInlineKeyboardButtonData("Commands", "commands"), + tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.commands"), "commands"), ), ) - var numericKeyboardClient = tgbotapi.NewInlineKeyboardMarkup( + numericKeyboardClient := tgbotapi.NewInlineKeyboardMarkup( tgbotapi.NewInlineKeyboardRow( - tgbotapi.NewInlineKeyboardButtonData("Get Usage", "client_traffic"), - tgbotapi.NewInlineKeyboardButtonData("Commands", "client_commands"), + tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.clientUsage"), "client_traffic"), + tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.commands"), "client_commands"), ), ) + var keyboardMarkup tgbotapi.InlineKeyboardMarkup if isAdmin { keyboardMarkup = numericKeyboard @@ -273,12 +286,18 @@ 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 { t.SendBackupToAdmins() @@ -295,22 +314,16 @@ func (t *Tgbot) SendBackupToAdmins() { } func (t *Tgbot) getServerUsage() string { - var info string - //get hostname - hostname, err := os.Hostname() - if err != nil { - logger.Error("get hostname error:", err) - hostname = "" - } - info = fmt.Sprintf("💻 Hostname: %s\r\n", hostname) - 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 { @@ -319,7 +332,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() + " " } @@ -327,18 +340,20 @@ 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 } @@ -353,22 +368,16 @@ func (t *Tgbot) UserLoginNotify(username string, ip string, time string, status return } - // Get hostname - hostname, err := os.Hostname() - if err != nil { - logger.Warning("get hostname error:", err) - return - } - msg := "" if status == LoginSuccess { - msg = fmt.Sprintf("✅ Successfully logged-in to the panel\r\nHostname:%s\r\n", hostname) + msg += t.I18nBot("tgbot.messages.loginSuccess") } else if status == LoginFail { - msg = fmt.Sprintf("❗ Login to the panel was unsuccessful\r\nHostname:%s\r\n", hostname) + 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) } @@ -379,17 +388,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")) } } } @@ -398,77 +409,91 @@ func (t *Tgbot) getInboundUsages() string { func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) { if len(tgUserName) == 0 { - msg := "Your configuration is not found!\nYou should configure your telegram username and ask Admin to add it to your configuration." + msg := t.I18nBot("tgbot.answers.askToAddUser") t.SendMsgToTgbot(chatId, msg) return } + traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserName) 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 in your configuration(s).\n\nYour username: @" + tgUserName + "" + msg := t.I18nBot("tgbot.answers.askToAddUserName", "TgUserName=="+tgUserName) 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.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) searchClient(chatId int64, email string) { 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.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) } @@ -477,47 +502,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 := "❌ No inbounds found!" + 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.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) } } @@ -527,35 +560,39 @@ 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.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) } @@ -605,41 +642,62 @@ func (t *Tgbot) getExhausted() string { } } + // Inbounds output := "" - output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds)) + 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.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" } } @@ -651,14 +709,16 @@ func (t *Tgbot) sendBackup(chatId int64) { return } - 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 := tgbotapi.FilePath(config.GetDBPath()) msg := tgbotapi.NewDocument(chatId, file) _, err := bot.Send(msg) if err != nil { logger.Warning("Error in uploading backup: ", err) } + file = tgbotapi.FilePath(xray.GetConfigPath()) msg = tgbotapi.NewDocument(chatId, file) _, err = bot.Send(msg) From 441efa65b5225c210b588a7fe4b4639b091cb368 Mon Sep 17 00:00:00 2001 From: Hamidreza Ghavami <70919649+hamid-gh98@users.noreply.github.com> Date: Sun, 21 May 2023 07:54:10 +0430 Subject: [PATCH 20/20] Update translations --- web/translation/translate.en_US.toml | 70 ++++++++++++++++++++++++++ web/translation/translate.fa_IR.toml | 70 ++++++++++++++++++++++++++ web/translation/translate.ru_RU.toml | 70 ++++++++++++++++++++++++++ web/translation/translate.zh_Hans.toml | 70 ++++++++++++++++++++++++++ 4 files changed, 280 insertions(+) diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index b536e90b..6e1eb15c 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -340,3 +340,73 @@ "modifyUser" = "Modify User " "originalUserPassIncorrect" = "Incorrect original username or password" "userPassMustBeNotEmpty" = "New username and new password cannot be empty" + +[tgbot] +"noResult" = "❗ No result!" +"wentWrong" = "❌ Something went wrong!" +"noInbounds" = "❗ No inbound found!" +"unlimited" = "♾ Unlimited" +"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 }}%" +"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" +"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" +"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" + +[tgbot.buttons] +"dbBackup" = "Get DB Backup" +"serverUsage" = "Server Usage" +"getInbounds" = "Get Inbounds" +"depleteSoon" = "Deplete soon" +"clientUsage" = "Get Usage" +"commands" = "Commands" + +[tgbot.answers] +"getInboundsFailed" = "❌ Failed to get inbounds" +"askToAddUser" = "Your configuration is not found!\r\nYou should configure your telegram username and ask your Admin to add it to your configuration." +"askToAddUserName" = "Your configuration is not found!\r\nPlease ask your Admin to use your telegram username in your configuration(s).\r\n\r\nYour username: @{{ .TgUserName }}" diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml index 57104b08..f5e64c6d 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -338,3 +338,73 @@ "modifyUser" = "ویرایش کاربر" "originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد " "userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد " + +[tgbot] +"noResult" = "❗ نتیجه‌ای یافت نشد!" +"wentWrong" = "❌ مشکلی رخ داده است!" +"noInbounds" = "❗ هیچ ورودی یافت نشد!" +"unlimited" = "♾ نامحدود" +"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 }}% است." +"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" +"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" +"exhaustedMsg" = "🚨 {{ .Type }} به اتمام رسیده است:\r\n" +"exhaustedCount" = "🚨 تعداد {{ .Type }} به اتمام رسیده:\r\n" +"disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n" +"depleteSoon" = "🔜 به زودی به پایان خواهد رسید: {{ .Deplete }}\r\n \r\n" +"backupTime" = "🗄 زمان پشتیبان‌گیری: {{ .Time }}\r\n" + +[tgbot.buttons] +"dbBackup" = "دریافت پشتیبان پایگاه داده" +"serverUsage" = "استفاده از سرور" +"getInbounds" = "دریافت ورودی‌ها" +"depleteSoon" = "به زودی به پایان خواهد رسید" +"clientUsage" = "دریافت آمار کاربر" +"commands" = "دستورات" + +[tgbot.answers] +"getInboundsFailed" = "❌ دریافت ورودی‌ها با خطا مواجه شد." +"askToAddUser" = "پیکربندی شما یافت نشد!\r\nشما باید نام کاربری تلگرام خود را پیکربندی کنید و از مدیر خود درخواست اضافه کردن آن به پیکربندی خود بکنید." +"askToAddUserName" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود درخواست استفاده از نام کاربری تلگرام خود در پیکربندی (ها) خود را بکنید.\r\n\r\nنام کاربری شما: @{{ .TgUserName }}" diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 6221c07f..82c6bad3 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -339,3 +339,73 @@ "modifyUser" = "Изменение пользователя " "originalUserPassIncorrect" = "Неверное имя пользователя или пароль" "userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены" + +[tgbot] +"noResult" = "❗ Нет результатов!" +"wentWrong" = "❌ Что-то пошло не так!" +"noInbounds" = "❗ Входящих соединений не найдено!" +"unlimited" = "♾ Неограниченно" +"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 }}%" +"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" +"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" +"exhaustedMsg" = "🚨 Исчерпаны {{ .Type }}:\r\n" +"exhaustedCount" = "🚨 Количество исчерпанных {{ .Type }}:\r\n" +"disabled" = "🛑 Отключено: {{ .Disabled }}\r\n" +"depleteSoon" = "🔜 Скоро исчерпание: {{ .Deplete }}\r\n \r\n" +"backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n" + +[tgbot.buttons] +"dbBackup" = "Получить резервную копию DB" +"serverUsage" = "Использование сервера" +"getInbounds" = "Получить входящие потоки" +"depleteSoon" = "Скоро исчерпание" +"clientUsage" = "Получить использование" +"commands" = "Команды" + +[tgbot.answers] +"getInboundsFailed" = "❌ Не удалось получить входящие потоки." +"askToAddUser" = "Конфигурация не найдена!\r\nВы должны настроить свое телеграм-имя пользователя и попросить вашего администратора добавить его в вашу конфигурацию." +"askToAddUserName" = "Конфигурация не найдена!\r\nПожалуйста, попросите вашего администратора использовать ваше телеграм-имя пользователя в вашей конфигурации(ях).\r\n\r\nВаше имя пользователя: @{{ .TgUserName }}" diff --git a/web/translation/translate.zh_Hans.toml b/web/translation/translate.zh_Hans.toml index 89847ffd..e3fe2922 100644 --- a/web/translation/translate.zh_Hans.toml +++ b/web/translation/translate.zh_Hans.toml @@ -339,3 +339,73 @@ "modifyUser" = "修改用户" "originalUserPassIncorrect" = "原用户名或原密码错误" "userPassMustBeNotEmpty" = "新用户名和新密码不能为空" + +[tgbot] +"noResult" = "❗ 没有结果!" +"wentWrong" = "❌ 出了点问题!" +"noInbounds" = "❗ 没有找到入站连接!" +"unlimited" = "♾ 无限制" +"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 }}%" +"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" +"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" +"exhaustedMsg" = "🚨 耗尽的{{ .Type }}:\r\n" +"exhaustedCount" = "🚨 耗尽的{{ .Type }}数量:\r\n" +"disabled" = "🛑 禁用:{{ .Disabled }}\r\n" +"depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n \r\n" +"backupTime" = "🗄 备份时间:{{ .Time }}\r\n" + +[tgbot.buttons] +"dbBackup" = "获取数据库备份" +"serverUsage" = "服务器使用情况" +"getInbounds" = "获取入站信息" +"depleteSoon" = "即将耗尽" +"clientUsage" = "获取使用情况" +"commands" = "命令" + +[tgbot.answers] +"getInboundsFailed" = "❌ 获取入站信息失败。" +"askToAddUser" = "找不到您的配置!\r\n您应该配置您的 Telegram 用户名,并要求管理员将其添加到您的配置中。" +"askToAddUserName" = "找不到您的配置!\r\n请要求您的管理员在您的配置中使用您的 Telegram 用户名。\r\n\r\n您的用户名:@{{ .TgUserName }}"