From e98be4f72adbb735bc4682b2b66877ea294f3875 Mon Sep 17 00:00:00 2001 From: BlindMaster24 <375291171150z@gmail.com> Date: Sun, 13 Sep 2026 20:48:14 +0300 Subject: [PATCH] fix(tgbot): render a disabled start-after-first-use client as days (#6500) A delayed-start expiry is stored as a negative duration, but the card checked the disabled-client branch before the sign of that duration, so it printed the epoch position (-2592000000 ms -> 1969-12-02) and labelled it an expire date. The sign decides first now, which is how BuildClientDraftMessage in this file, subscriptionExpiryFromClient and adjustTraffics already read the same value; the Discord card is the one surface still reading it as unlimited, fixed in #6498. --- internal/web/service/tgbot/tgbot_client.go | 8 ++- .../service/tgbot/tgbot_client_expiry_test.go | 58 +++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 internal/web/service/tgbot/tgbot_client_expiry_test.go diff --git a/internal/web/service/tgbot/tgbot_client.go b/internal/web/service/tgbot/tgbot_client.go index d6ff9f01c..07c4ffe59 100644 --- a/internal/web/service/tgbot/tgbot_client.go +++ b/internal/web/service/tgbot/tgbot_client.go @@ -442,6 +442,11 @@ func (t *Tgbot) clientInfoMsg( diff := traffic.ExpiryTime/1000 - now if traffic.ExpiryTime == 0 { expiryTime = t.I18nBot("tgbot.unlimited") + } else if traffic.ExpiryTime < 0 { + // A negative expiry counts days from first use, not a date; the disabled + // branch below would otherwise render it as a 1969 timestamp. + expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days")) + flag = true } else if diff > 172800 || !traffic.Enable { expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") if diff > 0 { @@ -460,9 +465,6 @@ func (t *Tgbot) clientInfoMsg( } expiryTime += fmt.Sprintf(" (%s)", remainingTime) } - } else if traffic.ExpiryTime < 0 { - expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days")) - flag = true } else { expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours")) flag = true diff --git a/internal/web/service/tgbot/tgbot_client_expiry_test.go b/internal/web/service/tgbot/tgbot_client_expiry_test.go new file mode 100644 index 000000000..745fc8951 --- /dev/null +++ b/internal/web/service/tgbot/tgbot_client_expiry_test.go @@ -0,0 +1,58 @@ +package tgbot + +import ( + "encoding/json" + "path/filepath" + "strings" + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/web/locale" + "github.com/mhsanaei/3x-ui/v3/internal/xray" + + "github.com/nicksnyder/go-i18n/v2/i18n" + "golang.org/x/text/language" +) + +// clientInfoLocalizer renders the lines clientInfoMsg prints with the templates +// the translation files carry; without it I18n returns the bare keys. +func clientInfoLocalizer(t *testing.T) { + t.Helper() + bundle := i18n.NewBundle(language.MustParse("en-US")) + bundle.RegisterUnmarshalFunc("json", json.Unmarshal) + _ = bundle.AddMessages(language.MustParse("en-US"), + &i18n.Message{ID: "tgbot.messages.email", Other: "Email: {{ .Email }}\r\n"}, + &i18n.Message{ID: "tgbot.days", Other: "Days"}, + &i18n.Message{ID: "tgbot.messages.expireIn", Other: "Expire In: {{ .Time }}\r\n"}, + &i18n.Message{ID: "tgbot.messages.expire", Other: "Expire Date: {{ .Time }}\r\n"}, + &i18n.Message{ID: "tgbot.wentWrong", Other: "went wrong"}, + ) + orig := locale.LocalizerBot + t.Cleanup(func() { locale.LocalizerBot = orig }) + locale.LocalizerBot = i18n.NewLocalizer(bundle, "en-US") +} + +// Regression test: a start-after-first-use client is stored as a negative duration, +// and a disabled one rendered it as a 1969 date. +func TestClientInfoShowsStartAfterFirstUseWhenDisabled(t *testing.T) { + if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { + t.Fatalf("InitDB: %v", err) + } + t.Cleanup(func() { _ = database.CloseDB() }) + clientInfoLocalizer(t) + + traffic := &xray.ClientTraffic{ + Email: "trial@example.com", + Enable: false, + ExpiryTime: -30 * 24 * 60 * 60000, + } + + out := (&Tgbot{}).clientInfoMsg(traffic, false, false, false, true, false, false) + + if strings.Contains(out, "1969") { + t.Errorf("client info = %q, want the days left, not a 1969 date", out) + } + if !strings.Contains(out, "Expire In: 30 Days") { + t.Errorf("client info = %q, want it to contain %q", out, "Expire In: 30 Days") + } +}