diff --git a/internal/web/service/discord/gateway.go b/internal/web/service/discord/gateway.go index b8c1f4d0a..d115ba47b 100644 --- a/internal/web/service/discord/gateway.go +++ b/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") diff --git a/internal/web/service/discord/gateway_delayed_expiry_test.go b/internal/web/service/discord/gateway_delayed_expiry_test.go new file mode 100644 index 000000000..2f2207915 --- /dev/null +++ b/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) + } + }) +}