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.
This commit is contained in:
BlindMaster24
2026-09-13 20:47:07 +03:00
committed by GitHub
parent 768bbd2a29
commit cc60cefe02
2 changed files with 117 additions and 1 deletions
+6 -1
View File
@@ -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")
@@ -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)
}
})
}