mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 11:22:12 +03:00
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:
@@ -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
internal/web/service/discord/gateway_delayed_expiry_test.go
Normal file
111
internal/web/service/discord/gateway_delayed_expiry_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user