diff --git a/sub/subController.go b/sub/subController.go index 69d9086d..b0e62ecf 100644 --- a/sub/subController.go +++ b/sub/subController.go @@ -3,12 +3,14 @@ package sub import ( "encoding/base64" "strings" + "x-ui/web/service" "github.com/gin-gonic/gin" ) type SUBController struct { - subService SubService + subService SubService + settingService service.SettingService } func NewSUBController(g *gin.RouterGroup) *SUBController { @@ -24,9 +26,10 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) { } func (a *SUBController) subs(c *gin.Context) { + subShowInfo, _ := a.settingService.GetSubShowInfo() subId := c.Param("subid") host := strings.Split(c.Request.Host, ":")[0] - subs, headers, err := a.subService.GetSubs(subId, host) + subs, headers, err := a.subService.GetSubs(subId, host, subShowInfo) if err != nil || len(subs) == 0 { c.String(400, "Error!") } else { diff --git a/sub/subService.go b/sub/subService.go index d82b8ceb..0565100e 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -18,12 +18,14 @@ import ( type SubService struct { address string + showInfo bool inboundService service.InboundService settingServics service.SettingService } -func (s *SubService) GetSubs(subId string, host string) ([]string, []string, error) { +func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string, []string, error) { s.address = host + s.showInfo = showInfo var result []string var headers []string var traffic xray.ClientTraffic @@ -57,7 +59,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, []string, err } for _, client := range clients { if client.Enable && client.SubID == subId { - link := s.getLink(inbound, client.Email, client.ExpiryTime) + link := s.getLink(inbound, client.Email) result = append(result, link) clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email)) } @@ -123,39 +125,26 @@ func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) { return inbound, nil } -func (s *SubService) getLink(inbound *model.Inbound, email string, expiryTime int64) string { +func (s *SubService) getLink(inbound *model.Inbound, email string) string { switch inbound.Protocol { case "vmess": - return s.genVmessLink(inbound, email, expiryTime) + return s.genVmessLink(inbound, email) case "vless": - return s.genVlessLink(inbound, email, expiryTime) + return s.genVlessLink(inbound, email) case "trojan": - return s.genTrojanLink(inbound, email, expiryTime) + return s.genTrojanLink(inbound, email) case "shadowsocks": - return s.genShadowsocksLink(inbound, email, expiryTime) + return s.genShadowsocksLink(inbound, email) } return "" } -func (s *SubService) genVmessLink(inbound *model.Inbound, email string, expiryTime int64) string { +func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { if inbound.Protocol != model.VMess { return "" } - - remainedTraffic := s.getRemainedTraffic(email) - expiryTimeString := getExpiryTime(expiryTime) - remark := "" - isTerminated := strings.Contains(expiryTimeString, "Terminated") || strings.Contains(remainedTraffic, "Terminated") - - if isTerminated { - remark = fmt.Sprintf("%s: %s⛔️", email, "Terminated") - } else { - remark = fmt.Sprintf("%s: %s - %s", email, remainedTraffic, expiryTimeString) - } - obj := map[string]interface{}{ "v": "2", - "ps": remark, "add": s.address, "port": inbound.Port, "type": "none", @@ -254,7 +243,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string, expiryTi links := "" for index, d := range domains { domain := d.(map[string]interface{}) - obj["ps"] = remark + "-" + domain["remark"].(string) + obj["ps"] = s.genRemark(inbound, email, domain["remark"].(string)) obj["add"] = domain["domain"].(string) if index > 0 { links += "\n" @@ -265,11 +254,13 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string, expiryTi return links } + obj["ps"] = s.genRemark(inbound, email, "") + jsonStr, _ := json.MarshalIndent(obj, "", " ") return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr) } -func (s *SubService) genVlessLink(inbound *model.Inbound, email string, expiryTime int64) string { +func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { address := s.address if inbound.Protocol != model.VLESS { return "" @@ -462,22 +453,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string, expiryTi // Set the new query values on the URL url.RawQuery = q.Encode() - remainedTraffic := s.getRemainedTraffic(email) - expiryTimeString := getExpiryTime(expiryTime) - remark := "" - isTerminated := strings.Contains(expiryTimeString, "Terminated") || strings.Contains(remainedTraffic, "Terminated") - - if isTerminated { - remark = fmt.Sprintf("%s: %s⛔️", email, "Terminated") - } else { - remark = fmt.Sprintf("%s: %s - %s", email, remainedTraffic, expiryTimeString) - } - if len(domains) > 0 { links := "" for index, d := range domains { domain := d.(map[string]interface{}) - url.Fragment = remark + "-" + domain["remark"].(string) + url.Fragment = s.genRemark(inbound, email, domain["remark"].(string)) url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port) if index > 0 { links += "\n" @@ -486,11 +466,12 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string, expiryTi } return links } - url.Fragment = remark + + url.Fragment = s.genRemark(inbound, email, "") return url.String() } -func (s *SubService) genTrojanLink(inbound *model.Inbound, email string, expiryTime int64) string { +func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string { address := s.address if inbound.Protocol != model.Trojan { return "" @@ -680,22 +661,11 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string, expiryT // Set the new query values on the URL url.RawQuery = q.Encode() - remainedTraffic := s.getRemainedTraffic(email) - expiryTimeString := getExpiryTime(expiryTime) - remark := "" - isTerminated := strings.Contains(expiryTimeString, "Terminated") || strings.Contains(remainedTraffic, "Terminated") - - if isTerminated { - remark = fmt.Sprintf("%s: %s⛔️", email, "Terminated") - } else { - remark = fmt.Sprintf("%s: %s - %s", email, remainedTraffic, expiryTimeString) - } - if len(domains) > 0 { links := "" for index, d := range domains { domain := d.(map[string]interface{}) - url.Fragment = remark + "-" + domain["remark"].(string) + url.Fragment = s.genRemark(inbound, email, domain["remark"].(string)) url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port) if index > 0 { links += "\n" @@ -705,11 +675,11 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string, expiryT return links } - url.Fragment = remark + url.Fragment = s.genRemark(inbound, email, "") return url.String() } -func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string, expiryTime int64) string { +func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string { address := s.address if inbound.Protocol != model.Shadowsocks { return "" @@ -788,22 +758,55 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string, ex // Set the new query values on the URL url.RawQuery = q.Encode() - - remainedTraffic := s.getRemainedTraffic(email) - expiryTimeString := getExpiryTime(expiryTime) - remark := "" - isTerminated := strings.Contains(expiryTimeString, "Terminated") || strings.Contains(remainedTraffic, "Terminated") - - if isTerminated { - remark = fmt.Sprintf("%s: %s⛔️", clients[clientIndex].Email, "Terminated") - } else { - remark = fmt.Sprintf("%s: %s - %s", clients[clientIndex].Email, remainedTraffic, expiryTimeString) - } - - url.Fragment = remark + url.Fragment = s.genRemark(inbound, email, "") return url.String() } +func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string { + var remark []string + if len(email) > 0 { + if len(inbound.Remark) > 0 { + remark = append(remark, inbound.Remark) + } + remark = append(remark, email) + if len(extra) > 0 { + remark = append(remark, extra) + } + } else { + return inbound.Remark + } + + if s.showInfo { + statsExist := false + var stats xray.ClientTraffic + for _, clientStat := range inbound.ClientStats { + if clientStat.Email == email { + stats = clientStat + statsExist = true + break + } + } + + // Get remained days + if statsExist { + if !stats.Enable { + return fmt.Sprintf("⛔️N/A-%s", strings.Join(remark, "-")) + } + if vol := stats.Total - (stats.Up + stats.Down); vol > 0 { + remark = append(remark, fmt.Sprintf("%s%s", common.FormatTraffic(vol), "📊")) + } + now := time.Now().Unix() + switch exp := stats.ExpiryTime / 1000; { + case exp > 0: + remark = append(remark, fmt.Sprintf("%d%s⏳", (exp-now)/86400, "Days")) + case exp < 0: + remark = append(remark, fmt.Sprintf("%d%s⏳", exp/-86400, "Days")) + } + } + } + return strings.Join(remark, "-") +} + func searchKey(data interface{}, key string) (interface{}, bool) { switch val := data.(type) { case map[string]interface{}: @@ -845,45 +848,3 @@ func searchHost(headers interface{}) string { return "" } - -func getExpiryTime(expiryTime int64) string { - now := time.Now().Unix() - expiryString := "" - - timeDifference := expiryTime/1000 - now - isTerminated := timeDifference/3600 <= 0 - - if expiryTime == 0 { - expiryString = "♾ ⏳" - } else if timeDifference > 172800 { - expiryString = fmt.Sprintf("%d %s⏳", timeDifference/86400, "Days") - } else if expiryTime < 0 { - expiryString = fmt.Sprintf("%d %s⏳", expiryTime/-86400000, "Days") - } else if isTerminated { - expiryString = fmt.Sprintf("%s⛔️", "Terminated") - } else { - expiryString = fmt.Sprintf("%d %s⏳", timeDifference/3600, "Hours") - } - - return expiryString -} - -func (s *SubService) getRemainedTraffic(email string) string { - traffic, err := s.inboundService.GetClientTrafficByEmail(email) - if err != nil { - logger.Warning(err) - } - - remainedTraffic := "" - isTerminated := traffic.Total-(traffic.Up+traffic.Down) < 0 - - if traffic.Total == 0 { - remainedTraffic = "♾ 📊" - } else if isTerminated { - remainedTraffic = fmt.Sprintf("%s⛔️", "Terminated") - } else { - remainedTraffic = fmt.Sprintf("%s%s", common.FormatTraffic(traffic.Total-(traffic.Up+traffic.Down)), "📊") - } - - return remainedTraffic -} diff --git a/web/assets/js/model/models.js b/web/assets/js/model/models.js index 2c992a1a..762d1174 100644 --- a/web/assets/js/model/models.js +++ b/web/assets/js/model/models.js @@ -194,6 +194,7 @@ class AllSetting { this.subCertFile = ""; this.subKeyFile = ""; this.subUpdates = 0; + this.subShowInfo = false; this.timeLocation = "Asia/Tehran"; diff --git a/web/controller/setting.go b/web/controller/setting.go index cd509293..d991633a 100644 --- a/web/controller/setting.go +++ b/web/controller/setting.go @@ -79,6 +79,7 @@ func (a *SettingController) getDefaultSettings(c *gin.Context) { "subDomain": func() (interface{}, error) { return a.settingService.GetSubDomain() }, "subKeyFile": func() (interface{}, error) { return a.settingService.GetSubKeyFile() }, "subCertFile": func() (interface{}, error) { return a.settingService.GetSubCertFile() }, + "subShowInfo": func() (interface{}, error) { return a.settingService.GetSubShowInfo() }, } result := make(map[string]interface{}) diff --git a/web/entity/entity.go b/web/entity/entity.go index 328af7b6..10a6a97d 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -55,6 +55,7 @@ type AllSetting struct { SubCertFile string `json:"subCertFile" form:"subCertFile"` SubKeyFile string `json:"subKeyFile" form:"subKeyFile"` SubUpdates int `json:"subUpdates" form:"subUpdates"` + SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"` } func (s *AllSetting) CheckValid() error { diff --git a/web/html/xui/settings.html b/web/html/xui/settings.html index 76ca8aa1..e1741302 100644 --- a/web/html/xui/settings.html +++ b/web/html/xui/settings.html @@ -406,6 +406,7 @@ + diff --git a/web/service/setting.go b/web/service/setting.go index 9bf74641..585e60c7 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -51,6 +51,7 @@ var defaultValueMap = map[string]string{ "subCertFile": "", "subKeyFile": "", "subUpdates": "12", + "subShowInfo": "false", } type SettingService struct { @@ -396,6 +397,10 @@ func (s *SettingService) GetSubUpdates() (int, error) { return s.getInt("subUpdates") } +func (s *SettingService) GetSubShowInfo() (bool, error) { + return s.getBool("subShowInfo") +} + func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error { if err := allSetting.CheckValid(); err != nil { return err diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index f760ee0b..5ac63f57 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -274,6 +274,8 @@ "subDomainDesc" = "Leave blank by default to monitor all domains and IPs" "subUpdates" = "Subscription update intervals" "subUpdatesDesc" = "Interval hours between updates in client application" +"subShowInfo" = "Show usage info" +"subShowInfoDesc" = "Show remianed traffic and date after config name" [pages.settings.templates] "title" = "Templates" diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml index 8557644f..a145965d 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -274,6 +274,8 @@ "subDomainDesc" = "برای نظارت بر همه دامنه ها و آی‌پی ها به طور پیش فرض خالی بگذارید" "subUpdates" = "فاصله به روز رسانی های سابسکریپشن" "subUpdatesDesc" = "ساعت های فاصله بین به روز رسانی در برنامه کاربر" +"subShowInfo" = "نمایش اطلاعات مصرف" +"subShowInfoDesc" = "ترافیک و زمان باقیمانده را در هر کانفیگ نمایش میدهد" [pages.settings.templates] "title" = "الگوها" diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 9d219129..53b6fedd 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -274,6 +274,8 @@ "subDomainDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все домены и IP-адреса" "subUpdates" = "Интервалы обновления подписки" "subUpdatesDesc" = "Часовой интервал между обновлениями в клиентском приложении" +"subShowInfo" = "Показать информацию об использовании" +"subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации" [pages.settings.templates] "title" = "Шаблоны" diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml index fc61909e..8ea0bfd2 100644 --- a/web/translation/translate.vi_VN.toml +++ b/web/translation/translate.vi_VN.toml @@ -274,6 +274,8 @@ "subDomainDesc" = "Mặc định để trống để nghe tất cả các tên miền và IP" "subUpdates" = "Khoảng thời gian cập nhật đăng ký" "subUpdatesDesc" = "Số giờ giữa các cập nhật trong ứng dụng khách" +"subShowInfo" = "Hiển thị thông tin sử dụng" +"subShowInfoDesc" = "Hiển thị lưu lượng truy cập còn lại và ngày sau tên cấu hình" [pages.settings.templates] "title" = "Mẫu" diff --git a/web/translation/translate.zh_Hans.toml b/web/translation/translate.zh_Hans.toml index b39e3ca8..fa9b84a3 100644 --- a/web/translation/translate.zh_Hans.toml +++ b/web/translation/translate.zh_Hans.toml @@ -274,6 +274,8 @@ "subDomainDesc" = "留空默认监控所有域名和IP" "subUpdates" = "订阅更新间隔" "subUpdatesDesc" = "客户端应用程序更新之间的间隔时间" +"subShowInfo" = "显示使用信息" +"subShowInfoDesc" = "在配置名称后显示剩余流量和日期" [pages.settings.templates] "title" = "模板"