ソースを参照

fix(sub): render the full remark once per subscription, not once per credential (#6198)

* fix(sub): scope full remarks to subscription identity

* fix(sub): preserve configured remark whitespace

---------

Co-authored-by: n0ctal <[email protected]>
n0ctal 18 時間 前
コミット
3b19091547
3 ファイル変更39 行追加9 行削除
  1. 12 4
      internal/sub/remark_vars.go
  2. 25 2
      internal/sub/remark_vars_test.go
  3. 2 3
      internal/sub/service.go

+ 12 - 4
internal/sub/remark_vars.go

@@ -607,19 +607,27 @@ func appendKeptRun(runs []string, run string, leftRemoved, rightRemoved bool) []
 	return runs
 }
 
-func (s *SubService) effectiveTemplate(email string) string {
+func templateInfoKey(client model.Client) string {
+	if client.SubID != "" {
+		return "sub:" + client.SubID
+	}
+	return "email:" + client.Email
+}
+
+func (s *SubService) effectiveTemplate(client model.Client) string {
 	translated := translateUISingleBrackets(s.remarkTemplate)
 	if s.usageShown == nil {
 		s.usageShown = map[string]bool{}
 	}
-	if s.usageShown[email] {
+	key := templateInfoKey(client)
+	if s.usageShown[key] {
 		remove := firstLinkOnlyBodyTokens
 		if s.showIdentityOnAllLinks {
 			remove = usageInfoTokens
 		}
 		return filterRemarkTemplate(translated, remove)
 	}
-	s.usageShown[email] = true
+	s.usageShown[key] = true
 	return translated
 }
 
@@ -646,7 +654,7 @@ func (s *SubService) genTemplatedRemark(inbound *model.Inbound, client model.Cli
 	}
 	var tmpl string
 	if s.subscriptionBody {
-		tmpl = s.effectiveTemplate(client.Email)
+		tmpl = s.effectiveTemplate(client)
 	} else {
 		tmpl = filterRemarkTemplate(translateUISingleBrackets(s.remarkTemplate), displayRemoveTokens)
 	}

+ 25 - 2
internal/sub/remark_vars_test.go

@@ -649,7 +649,6 @@ func TestUsageOnFirstLinkOnly_SingleBracket(t *testing.T) {
 	}
 	client := model.Client{Email: "alice@x"}
 	first := s.genTemplatedRemark(inbound, client, "", "ws")
-	s.usageShown["alice@x"] = true
 	second := s.genTemplatedRemark(inbound, client, "", "ws")
 	if !strings.Contains(first, "📊") {
 		t.Fatalf("first link should carry usage: %q", first)
@@ -675,7 +674,6 @@ func TestEmailOnFirstLinkOnly(t *testing.T) {
 	}
 	client := model.Client{Email: "alice@x"}
 	first := s.genTemplatedRemark(inbound, client, "", "ws")
-	s.usageShown["alice@x"] = true
 	second := s.genTemplatedRemark(inbound, client, "", "ws")
 	if !strings.Contains(first, "alice@x") {
 		t.Fatalf("first link should carry email: %q", first)
@@ -724,3 +722,28 @@ func TestIdentityOnAllLinks(t *testing.T) {
 		})
 	}
 }
+
+func TestSharedSubIDRemark_FullInfoOncePerSubscription(t *testing.T) {
+	const tmpl = "{{INBOUND}}-{{EMAIL}}"
+	s := &SubService{
+		remarkTemplate:   tmpl,
+		subscriptionBody: true,
+		usageShown:       map[string]bool{},
+	}
+	first := model.Client{Email: "first@example", SubID: "shared-sub"}
+	second := model.Client{Email: "second@example", SubID: "shared-sub"}
+	if got := s.genTemplatedRemark(&model.Inbound{Remark: "DE"}, first, "", "tcp"); got != "DE-first@example" {
+		t.Fatalf("first credential remark = %q", got)
+	}
+	if got := s.genTemplatedRemark(&model.Inbound{Remark: "FI"}, second, "", "tcp"); got != "FI" {
+		t.Fatalf("second credential with shared subId remark = %q, want identity suppressed", got)
+	}
+}
+
+func TestGenTemplatedRemarkPreservesConfiguredOuterWhitespace(t *testing.T) {
+	s := &SubService{remarkTemplate: "  {{INBOUND}}  ", subscriptionBody: true, usageShown: map[string]bool{}}
+	got := s.genTemplatedRemark(&model.Inbound{Remark: "DE"}, model.Client{Email: "[email protected]"}, "", "tcp")
+	if got != "  DE  " {
+		t.Fatalf("remark = %q, want configured outer whitespace preserved", got)
+	}
+}

+ 2 - 3
internal/sub/service.go

@@ -37,9 +37,8 @@ type SubService struct {
 	// other context — the sub info page, the panel's link/QR displays — renders
 	// the name-only template, like Remnawave.
 	subscriptionBody bool
-	// usageShown tracks, per client email, whether the info part of the template
-	// has already been emitted this request, so it appears on the first body
-	// link only. Per-request state; reset in PrepareForRequest.
+	// usageShown emits info once per subscription identity, including twins.
+	// PrepareForRequest resets this per-request state.
 	usageShown             map[string]bool
 	showIdentityOnAllLinks bool
 	inboundService         service.InboundService