fix(tgbot): let clients page their screens and see usage per account (#7033)

pg:next, pg:prev and pg:none were missing from isClientSelfCallback, so the callback gate dropped every page tap from a non-admin and a client with several accounts was stuck on page one. Paging only re-renders the screen stored for the caller's own chat, so the gate now lets it through. An account's links screen also shows that account's usage to its owner instead of the admin-only client card button. Fixes #7032.
This commit is contained in:
Бат-Орших
2026-11-02 22:27:40 +08:00
committed by GitHub
parent 77b56a2178
commit a712bf1413
3 changed files with 76 additions and 9 deletions
@@ -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))))
}
@@ -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)
}
}
@@ -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