diff --git a/internal/web/service/tgbot/screen_client_views.go b/internal/web/service/tgbot/screen_client_views.go index 9a2136a2d..438e36a1c 100644 --- a/internal/web/service/tgbot/screen_client_views.go +++ b/internal/web/service/tgbot/screen_client_views.go @@ -10,6 +10,7 @@ import ( "time" "github.com/mhsanaei/3x-ui/v3/internal/logger" + "github.com/mhsanaei/3x-ui/v3/internal/xray" "github.com/mymmrac/telego" tu "github.com/mymmrac/telego/telegoutil" @@ -137,17 +138,22 @@ func (t *Tgbot) sendTGPicker(chatID int64, trafficID int) { }) } -// ownSubscriptionOnly reports whether the screen is the client's own single -// subscription, where admin-only controls must stay off (reached via "Links"). -func (t *Tgbot) ownSubscriptionOnly(chatID int64, email string) bool { +// ownClientTraffic returns the usage of a client the chat's user owns, and nil +// for an admin: the owner sees its usage here, the admin has the client card. +func (t *Tgbot) ownClientTraffic(chatID int64, email string) *xray.ClientTraffic { if checkAdmin(chatID) { - return false + return nil } traffics, err := t.inboundService.GetClientTrafficTgBot(chatID) if err != nil { - return false + return nil } - return len(traffics) == 1 && traffics[0].Email == email + for _, traffic := range traffics { + if traffic.Email == email { + return traffic + } + } + return nil } // screenClientLinks draws the subscription and the links of a client. @@ -158,6 +164,10 @@ func (t *Tgbot) screenClientLinks(chatID int64, email string) { return } body := t.clientScreenBody(email, "") + own := t.ownClientTraffic(chatID, email) + if own != nil { + body = t.clientInfoMsg(own, true, true, true, true, true, false) + "\n" + } body += t.I18nBot("tgbot.messages.subURL", "URL=="+subURL) if subJSON != "" { body += t.I18nBot("tgbot.messages.jsonURL", "URL=="+subJSON) @@ -167,9 +177,8 @@ func (t *Tgbot) screenClientLinks(chatID int64, email string) { tu.InlineKeyboardRow(t.btn("tgbot.buttons.individualLinks", t.encodeQuery("client_individual_links "+email))), tu.InlineKeyboardRow(t.btn("tgbot.buttons.qrCode", t.encodeQuery("client_qr_links "+email))), } - // A client looking at its own subscription has nothing behind the admin's - // card: it only repeats the usage already on its home screen. - if !t.ownSubscriptionOnly(chatID, email) { + // The client card is admin-only; a client's own usage is already above. + if own == nil { rows = append(rows, tu.InlineKeyboardRow( t.btn("tgbot.buttons.clientCard", t.encodeQuery("client_get_usage "+email)))) } diff --git a/internal/web/service/tgbot/screens_test.go b/internal/web/service/tgbot/screens_test.go index 048c0de4d..9c45b237e 100644 --- a/internal/web/service/tgbot/screens_test.go +++ b/internal/web/service/tgbot/screens_test.go @@ -18,6 +18,7 @@ import ( "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" + "github.com/mhsanaei/3x-ui/v3/internal/util/common" "github.com/mhsanaei/3x-ui/v3/internal/web/locale" "github.com/mhsanaei/3x-ui/v3/internal/xray" @@ -1142,3 +1143,57 @@ func TestIndividualLinksScreenHasNoDuplicateLinksButton(t *testing.T) { t.Errorf("the only control is %q, want the back row %q", got, cbHome) } } + +// The pager was drawn on a client's screen but its taps never got past the +// callback gate, so a client with several accounts was stuck on page one. +func TestClientCanTurnPages(t *testing.T) { + tb, _ := newScreenTgbot(t, false) + initReportDB(t) + setTestAdmins(t) + seedClientForTgUser(t, 4848, "pager@x") + + for _, data := range []string{cbPageNext, cbPagePrev, cbPageNone} { + if !isClientSelfCallback(data) { + t.Errorf("a client's %q button is refused by the callback gate", data) + } + } + + tb.renderScreen(4848, tb.newScreen("main", strings.Repeat("line of text\n", 300))) + tb.answerCallback(&telego.CallbackQuery{ + ID: "q1", + From: telego.User{ID: 4848}, + Data: cbPageNext, + Message: &telego.Message{Chat: telego.Chat{ID: 4848}}, + }, false) + + sc, ok := tb.screens().get(4848) + if !ok { + t.Fatal("no screen was tracked") + } + if sc.page != 1 { + t.Errorf("page = %d after a client tapped next, want 1", sc.page) + } +} + +// A client with several accounts opens one from its home screen: that screen +// must show the account's usage, since the client card is admin-only. +func TestLinksScreenShowsUsageToItsOwner(t *testing.T) { + tb, _ := newScreenTgbot(t, false) + initReportDB(t) + setTestAdmins(t) + seedClientForTgUser(t, 4949, "first@x") + seedClientForTgUser(t, 4949, "second@x") + + tb.screenClientLinks(4949, "second@x") + sc, ok := tb.screens().get(4949) + if !ok { + t.Fatal("no screen was tracked") + } + if want := tb.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(0)); !strings.Contains(sc.body, want) { + t.Errorf("the client's links screen shows no usage:\n%s", sc.body) + } + markup, _ := json.Marshal(sc.markup) + if strings.Contains(string(markup), "client_get_usage") { + t.Errorf("a client is offered the admin's client card: %s", markup) + } +} diff --git a/internal/web/service/tgbot/tgbot_router.go b/internal/web/service/tgbot/tgbot_router.go index 206ca5c39..b91178473 100644 --- a/internal/web/service/tgbot/tgbot_router.go +++ b/internal/web/service/tgbot/tgbot_router.go @@ -1241,6 +1241,9 @@ func isClientSelfCallback(data string) bool { case "home", "hide", "client_traffic", "client_commands", "client_sub_links", "client_individual_links", "client_qr_links": return true + // Paging only turns the screen already tracked for the caller's own chat. + case cbPageNext, cbPagePrev, cbPageNone: + return true } _, _, ok := splitClientLinkCallback(data) return ok