feat(sub): add dynamic remark variables with Jalali date, transport, and status tokens (#5430)

* feat(sub): implement dynamic single-bracket remark variables with timezone-aware inline Jalali conversion

* Update .gitignore

* Update .gitignore

* merge: bring in origin/main commits to resolve conflict base

* fix(sub): address review issues in dynamic remark variables

- Add TIME_LEFT to unlimitedDropTokens so segments containing only
  {TIME_LEFT} are dropped for unlimited clients (same as DAYS_LEFT)
- Remove dead uiSingleBraceRe variable (translateUISingleBrackets uses
  a character scanner, not this regex)
- Change expireDateLabel to use time.Local instead of UTC, consistent
  with jalaliExpireDateLabel

Co-authored-by: Sanaei <MHSanaei@users.noreply.github.com>

* fix

* fix

---------

Co-authored-by: MHSanaei <MHSanaei@users.noreply.github.com>
This commit is contained in:
wahh3b-lgtm
2026-06-21 03:30:27 +03:30
committed by GitHub
parent ce1d348ece
commit 605e90dbf0
11 changed files with 447 additions and 56 deletions
+16 -15
View File
@@ -532,10 +532,10 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
externalProxies, _ := stream["externalProxy"].([]any)
if len(externalProxies) > 0 {
return s.buildVmessExternalProxyLinks(externalProxies, obj, inbound, email)
return s.buildVmessExternalProxyLinks(externalProxies, obj, inbound, email, network)
}
obj["ps"] = s.genRemark(inbound, email, "")
obj["ps"] = s.genRemark(inbound, email, "", network)
return buildVmessLink(obj)
}
@@ -619,13 +619,13 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
return fmt.Sprintf("vless://%s@%s", uuid, joinHostPort(dest, port))
},
func(ep map[string]any) string {
return s.endpointRemark(inbound, email, ep)
return s.endpointRemark(inbound, email, ep, streamNetwork)
},
)
}
link := fmt.Sprintf("vless://%s@%s", uuid, joinHostPort(address, port))
return buildLinkWithParams(link, params, s.genRemark(inbound, email, ""))
return buildLinkWithParams(link, params, s.genRemark(inbound, email, "", streamNetwork))
}
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
@@ -670,13 +670,13 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
return fmt.Sprintf("trojan://%s@%s", password, joinHostPort(dest, port))
},
func(ep map[string]any) string {
return s.endpointRemark(inbound, email, ep)
return s.endpointRemark(inbound, email, ep, streamNetwork)
},
)
}
link := fmt.Sprintf("trojan://%s@%s", password, joinHostPort(address, port))
return buildLinkWithParams(link, params, s.genRemark(inbound, email, ""))
return buildLinkWithParams(link, params, s.genRemark(inbound, email, "", streamNetwork))
}
// encodeUserinfo percent-encodes a userinfo (password/auth) value so it
@@ -763,13 +763,13 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
return fmt.Sprintf("ss://%s@%s", userInfo, joinHostPort(dest, port))
},
func(ep map[string]any) string {
return s.endpointRemark(inbound, email, ep)
return s.endpointRemark(inbound, email, ep, streamNetwork)
},
)
}
link := fmt.Sprintf("ss://%s@%s", userInfo, joinHostPort(address, inbound.Port))
return buildLinkWithParams(link, params, s.genRemark(inbound, email, ""))
return buildLinkWithParams(link, params, s.genRemark(inbound, email, "", streamNetwork))
}
func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) string {
@@ -872,7 +872,7 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin
applyExternalProxyHysteriaParams(ep, epParams)
link := fmt.Sprintf("%s://%s@%s", protocol, auth, joinHostPort(dest, int(portF)))
links = append(links, buildLinkWithParams(link, epParams, s.endpointRemark(inbound, email, ep)))
links = append(links, buildLinkWithParams(link, epParams, s.endpointRemark(inbound, email, ep, "quic")))
}
return strings.Join(links, "\n")
}
@@ -883,7 +883,7 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin
params["mport"] = hopPorts
}
link := fmt.Sprintf("%s://%s@%s", protocol, auth, joinHostPort(s.resolveInboundAddress(inbound), inbound.Port))
return buildLinkWithParams(link, params, s.genRemark(inbound, email, ""))
return buildLinkWithParams(link, params, s.genRemark(inbound, email, "", "quic"))
}
// hysteriaHopPorts returns the configured Hysteria2 UDP port-hopping range
@@ -1494,14 +1494,15 @@ func joinAnyStrings(items []any) string {
// buildVmessExternalProxyLinks is a thin adapter: it maps the legacy
// externalProxy entries to []ShareEndpoint and renders them through the unified
// endpoint path. Kept so genVmessLink's call site is unchanged.
func (s *SubService) buildVmessExternalProxyLinks(externalProxies []any, baseObj map[string]any, inbound *model.Inbound, email string) string {
// endpoint path. Kept as a thin shim over the unified endpoint builder so
// genVmessLink keeps calling one helper (now threading transport through).
func (s *SubService) buildVmessExternalProxyLinks(externalProxies []any, baseObj map[string]any, inbound *model.Inbound, email string, transport string) string {
eps := make([]ShareEndpoint, 0, len(externalProxies))
for _, externalProxy := range externalProxies {
ep, _ := externalProxy.(map[string]any)
eps = append(eps, externalProxyToEndpoint(ep))
}
return s.buildEndpointVmessLinks(eps, baseObj, inbound, email)
return s.buildEndpointVmessLinks(eps, baseObj, inbound, email, transport)
}
// buildLinkWithParams appends ?query and #fragment to a pre-built
@@ -1588,9 +1589,9 @@ func cloneStringMap(source map[string]string) map[string]string {
// externalProxy / synthetic JSON-Clash entry). In the subscription body a set
// remark template takes over; otherwise (and in every display context) the
// remark is just the config name (inbound remark, then extra).
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string, transport string) string {
if s.remarkTemplate != "" && s.subscriptionBody {
return s.genTemplatedRemark(inbound, s.lookupClient(inbound, email), extra)
return s.genTemplatedRemark(inbound, s.lookupClient(inbound, email), extra, transport)
}
// Sub info page + panel link/QR displays: just the config name (no template,
// so no per-client email/usage leaks into the shown remark).