Преглед на файлове

fix(discord): report a start-after-first-use client as days, not unlimited (#6498)

!usage printed Unlimited for any client whose expiry was not a positive
timestamp, but the panel stores "Start After First Use" as the duration
negated and converts it on the first traffic tick. Such a client does expire,
so the operator reading that embed was told the opposite of what the panel
and the Telegram bot already say, which both render the same value as days.
BlindMaster24 преди 14 часа
родител
ревизия
cc60cefe02
променени са 2 файла, в които са добавени 117 реда и са изтрити 1 реда
  1. 6 1
      internal/web/service/discord/gateway.go
  2. 111 0
      internal/web/service/discord/gateway_delayed_expiry_test.go

+ 6 - 1
internal/web/service/discord/gateway.go

@@ -577,8 +577,13 @@ func (g *GatewayClient) sendUsage(ctx context.Context, email string) {
 				}
 
 				expireStr := tr("unlimited")
-				if client.ExpiryTime > 0 {
+				switch {
+				case client.ExpiryTime > 0:
 					expireStr = time.Unix(client.ExpiryTime/1000, 0).Format("2006-01-02 15:04:05")
+				// Start After First Use stores the duration negated, so such a client is
+				// not unlimited: it starts counting down on its first connection.
+				case client.ExpiryTime < 0:
+					expireStr = fmt.Sprintf("%d %s", client.ExpiryTime/-86400000, tr("tgbot.days"))
 				}
 
 				totalLimitStr := tr("unlimited")

+ 111 - 0
internal/web/service/discord/gateway_delayed_expiry_test.go

@@ -0,0 +1,111 @@
+package discord
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"sync"
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
+	"github.com/mhsanaei/3x-ui/v3/internal/xray"
+)
+
+// expiryField is the position of the expiry field in the usage embed.
+const expiryField = 5
+
+func runUsageCommand(t *testing.T, settingService service.SettingService, inbounds []*model.Inbound, email string) MessagePayload {
+	t.Helper()
+	var mu sync.Mutex
+	var sent []MessagePayload
+	restServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		var payload MessagePayload
+		_ = json.NewDecoder(r.Body).Decode(&payload)
+		mu.Lock()
+		sent = append(sent, payload)
+		mu.Unlock()
+		w.WriteHeader(http.StatusOK)
+		_, _ = w.Write([]byte(`{"id": "msg-1"}`))
+	}))
+	t.Cleanup(restServer.Close)
+
+	svc := NewDiscordService(settingService)
+	svc.SetBaseURL(restServer.URL)
+	svc.SetHTTPClient(restServer.Client())
+
+	gw := NewGatewayClient(svc, settingService, &mockServerProvider{}, &mockInboundProvider{inbounds: inbounds}, &mockXrayRestart{})
+	gw.handleMessage(context.Background(), MessageCreateData{
+		ID:        "m1",
+		ChannelID: "ch-1",
+		Content:   "!usage " + email,
+		Author: struct {
+			ID       string `json:"id"`
+			Username string `json:"username"`
+			Bot      bool   `json:"bot"`
+		}{ID: "u1", Username: "Alice"},
+	})
+
+	mu.Lock()
+	defer mu.Unlock()
+	if len(sent) == 0 {
+		t.Fatalf("!usage %s sent nothing", email)
+	}
+	return sent[0]
+}
+
+func usageExpiryValue(t *testing.T, payload MessagePayload) string {
+	t.Helper()
+	if len(payload.Embeds) != 1 {
+		t.Fatalf("expected one embed, got %d", len(payload.Embeds))
+	}
+	fields := payload.Embeds[0].Fields
+	if len(fields) <= expiryField {
+		t.Fatalf("usage embed has %d fields, want at least %d", len(fields), expiryField+1)
+	}
+	return fields[expiryField].Value
+}
+
+func TestUsageExpiryForDelayedStart(t *testing.T) {
+	settingService := setupTestDB(t)
+	_ = settingService.SetDiscordBotToken("test-bot-token")
+	_ = settingService.SetDiscordChannelId("ch-1")
+	_ = settingService.SetDiscordAdminIds("u1")
+
+	const email = "delayed@test"
+
+	t.Run("a start-after-first-use client counts down in days", func(t *testing.T) {
+		// -2592000000 ms is what the panel stores for "Start After First Use: 30 days".
+		inbounds := []*model.Inbound{{
+			Id:          1,
+			Remark:      "delayed",
+			Port:        443,
+			Protocol:    "vless",
+			Enable:      true,
+			ClientStats: []xray.ClientTraffic{{Email: email, Enable: true, ExpiryTime: -2592000000}},
+		}}
+
+		got := usageExpiryValue(t, runUsageCommand(t, settingService, inbounds, email))
+		if got != "30 Days" {
+			t.Errorf("delayed start shows %q, want %q", got, "30 Days")
+		}
+	})
+
+	t.Run("an absolute deadline still shows as a date", func(t *testing.T) {
+		const deadline = int64(4102444800000) // 2100-01-01 UTC, in ms
+		inbounds := []*model.Inbound{{
+			Id:          2,
+			Remark:      "deadline",
+			Port:        8443,
+			Protocol:    "vless",
+			Enable:      true,
+			ClientStats: []xray.ClientTraffic{{Email: email, Enable: true, ExpiryTime: deadline}},
+		}}
+
+		got := usageExpiryValue(t, runUsageCommand(t, settingService, inbounds, email))
+		if got == "Unlimited" || got == "30 Days" {
+			t.Errorf("absolute deadline shows %q, want a formatted date", got)
+		}
+	})
+}