mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 15:47:14 +00:00
feat(sub): add dummy info node and status configs for subscriptions (#6412)
* feat(settings): add subInfoNodeEnable and status template settings * feat(sub): add dummy info node and status configs for raw links * feat(sub): support dummy info node in clash and json subscriptions * feat(ui): add subscription info node switch and status templates to settings * style: apply gofumpt formatting * fix(sub): address review feedback on subscription info node - Restore GetSubs contract to avoid unintended remark expansions on non-subscription-body calls. - Exclude dummy info node from Clash PROXY select group when active server nodes exist. - Track hasEnabledClient and set traffic.Enable in JSON and Clash paths so status tokens evaluate correctly. - Deterministically sort client emails across subscriptions before selecting primaryEmail. - Consolidate duplicated info-node evaluation logic into resolveInfoNodeRemark helper. - Remove redundant pure-getter test from setting_sub_info_node_test.go. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com>
This commit is contained in:
+102
-10
@@ -44,10 +44,13 @@ type SubService struct {
|
||||
subscriptionBody bool
|
||||
// usageShown emits info once per subscription identity, including twins.
|
||||
// PrepareForRequest resets this per-request state.
|
||||
usageShown map[string]bool
|
||||
showIdentityOnAllLinks bool
|
||||
inboundService service.InboundService
|
||||
settingService service.SettingService
|
||||
usageShown map[string]bool
|
||||
showIdentityOnAllLinks bool
|
||||
subInfoNodeEnable bool
|
||||
subExpiredTemplate string
|
||||
subTrafficDepletedTemplate string
|
||||
inboundService service.InboundService
|
||||
settingService service.SettingService
|
||||
// nodesByID is populated per request from the Node table so
|
||||
// resolveInboundAddress can return the node's address for any
|
||||
// inbound whose NodeID is set. Keeps the per-link host derivation
|
||||
@@ -197,14 +200,29 @@ func (s *SubService) linkSettings(inbound *model.Inbound) map[string]any {
|
||||
// (the date formatter reads datepicker). Loading it only in getSubs left
|
||||
// JSON/Clash with the zero value.
|
||||
func (s *SubService) loadRemarkSettings() {
|
||||
var err error
|
||||
s.datepicker, err = s.settingService.GetDatepicker()
|
||||
if err != nil {
|
||||
if s.datepicker == "" {
|
||||
s.datepicker = "gregorian"
|
||||
}
|
||||
s.showIdentityOnAllLinks, err = s.settingService.GetSubShowIdentityOnAllLinks()
|
||||
if err != nil {
|
||||
s.showIdentityOnAllLinks = false
|
||||
if s.subExpiredTemplate == "" {
|
||||
s.subExpiredTemplate = service.DefaultSubExpiredTemplate
|
||||
}
|
||||
if s.subTrafficDepletedTemplate == "" {
|
||||
s.subTrafficDepletedTemplate = service.DefaultSubTrafficDepletedTemplate
|
||||
}
|
||||
if datepicker, err := s.settingService.GetDatepicker(); err == nil && datepicker != "" {
|
||||
s.datepicker = datepicker
|
||||
}
|
||||
if enabled, err := s.settingService.GetSubShowIdentityOnAllLinks(); err == nil && enabled {
|
||||
s.showIdentityOnAllLinks = enabled
|
||||
}
|
||||
if enabled, err := s.settingService.GetSubInfoNodeEnable(); err == nil && enabled {
|
||||
s.subInfoNodeEnable = enabled
|
||||
}
|
||||
if tmpl, err := s.settingService.GetSubExpiredTemplate(); err == nil && tmpl != "" {
|
||||
s.subExpiredTemplate = tmpl
|
||||
}
|
||||
if tmpl, err := s.settingService.GetSubTrafficDepletedTemplate(); err == nil && tmpl != "" {
|
||||
s.subTrafficDepletedTemplate = tmpl
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,6 +311,70 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, []string, int
|
||||
return s.ForRequest(host).getSubs(subId)
|
||||
}
|
||||
|
||||
type infoNodeMode int
|
||||
|
||||
const (
|
||||
infoNodeNone infoNodeMode = iota
|
||||
infoNodeActive
|
||||
infoNodeExpired
|
||||
infoNodeDepleted
|
||||
)
|
||||
|
||||
func (s *SubService) resolveInfoNodeRemark(subId string, uniqueEmails []string, traffic xray.ClientTraffic, hasEntries bool) (infoNodeMode, string) {
|
||||
if !s.subInfoNodeEnable || !s.subscriptionBody {
|
||||
return infoNodeNone, ""
|
||||
}
|
||||
nowSec := time.Now().Unix()
|
||||
isExpired := traffic.ExpiryTime > 0 && traffic.ExpiryTime/1000 <= nowSec
|
||||
isDepleted := traffic.Total > 0 && (traffic.Up+traffic.Down) >= traffic.Total
|
||||
|
||||
primaryEmail := ""
|
||||
if len(uniqueEmails) > 0 {
|
||||
primaryEmail = uniqueEmails[0]
|
||||
}
|
||||
ctx := remarkContext{
|
||||
client: model.Client{Email: primaryEmail, SubID: subId},
|
||||
stats: traffic,
|
||||
}
|
||||
|
||||
if isExpired {
|
||||
tmpl := s.subExpiredTemplate
|
||||
if tmpl == "" {
|
||||
tmpl = service.DefaultSubExpiredTemplate
|
||||
}
|
||||
remark := expandRemarkVars(tmpl, ctx)
|
||||
if strings.TrimSpace(remark) == "" {
|
||||
remark = "Expired"
|
||||
}
|
||||
return infoNodeExpired, remark
|
||||
}
|
||||
|
||||
if isDepleted {
|
||||
tmpl := s.subTrafficDepletedTemplate
|
||||
if tmpl == "" {
|
||||
tmpl = service.DefaultSubTrafficDepletedTemplate
|
||||
}
|
||||
remark := expandRemarkVars(tmpl, ctx)
|
||||
if strings.TrimSpace(remark) == "" {
|
||||
remark = "Traffic Depleted"
|
||||
}
|
||||
return infoNodeDepleted, remark
|
||||
}
|
||||
|
||||
if hasEntries {
|
||||
tmpl := s.remarkTemplate
|
||||
if tmpl == "" {
|
||||
tmpl = service.DefaultRemarkTemplate
|
||||
}
|
||||
remark := expandRemarkVars(tmpl, ctx)
|
||||
if strings.TrimSpace(remark) != "" {
|
||||
return infoNodeActive, remark
|
||||
}
|
||||
}
|
||||
|
||||
return infoNodeNone, ""
|
||||
}
|
||||
|
||||
func (s *SubService) getSubs(subId string) ([]string, []string, int64, xray.ClientTraffic, error) {
|
||||
var result []string
|
||||
var emails []string
|
||||
@@ -360,8 +442,18 @@ func (s *SubService) getSubs(subId string) ([]string, []string, int64, xray.Clie
|
||||
for e := range seenEmails {
|
||||
uniqueEmails = append(uniqueEmails, e)
|
||||
}
|
||||
slices.Sort(uniqueEmails)
|
||||
traffic, lastOnline := s.AggregateTrafficByEmails(uniqueEmails)
|
||||
traffic.Enable = hasEnabledClient
|
||||
|
||||
if mode, remark := s.resolveInfoNodeRemark(subId, uniqueEmails, traffic, len(result) > 0); mode != infoNodeNone {
|
||||
dummyLink := fmt.Sprintf("socks://127.0.0.1:1080#%s", strings.ReplaceAll(url.QueryEscape(remark), "+", "%20"))
|
||||
if mode == infoNodeExpired || mode == infoNodeDepleted {
|
||||
return []string{dummyLink}, emails, lastOnline, traffic, nil
|
||||
}
|
||||
result = append([]string{dummyLink}, result...)
|
||||
}
|
||||
|
||||
return result, emails, lastOnline, traffic, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user