mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-03 19:55:04 +03:00
fix(tgbot): restore the admin's client link and QR buttons (#6986)
The v3.11.0 bot rebuild left the admin's client card without the subscription link, individual links and QR code, so an admin could not hand a config to a client who cannot reach Telegram yet. The card now carries Links and QR code buttons that open the existing screens. Closes #6984.
This commit is contained in:
@@ -76,8 +76,11 @@ admins are notified when one runs out.
|
|||||||
|
|
||||||
Admins also get inline-button flows for server usage, sorted traffic reports,
|
Admins also get inline-button flows for server usage, sorted traffic reports,
|
||||||
resetting traffic, DB backups, ban logs, listing inbounds/clients, online
|
resetting traffic, DB backups, ban logs, listing inbounds/clients, online
|
||||||
clients, "depleting soon", and a full **add-client** wizard. Regular users get
|
clients, "depleting soon", and a full **add-client** wizard. The admin's
|
||||||
buttons for their own usage, subscription links, individual links, and QR codes.
|
**client card** carries the client's subscription link, individual links and QR
|
||||||
|
code, so a config can be handed to a client who cannot reach Telegram yet.
|
||||||
|
Regular users get buttons for their own usage, subscription links, individual
|
||||||
|
links, and QR codes.
|
||||||
|
|
||||||
## Reports & alerts
|
## Reports & alerts
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ icon: Send
|
|||||||
ادمینها همچنین جریانهای دکمهی درونخطی برای مصرف سرور، گزارشهای ترافیک مرتبشده،
|
ادمینها همچنین جریانهای دکمهی درونخطی برای مصرف سرور، گزارشهای ترافیک مرتبشده،
|
||||||
بازنشانی ترافیک، پشتیبانگیری از DB، گزارشهای مسدودسازی، فهرست کردن ورودیها/کلاینتها،
|
بازنشانی ترافیک، پشتیبانگیری از DB، گزارشهای مسدودسازی، فهرست کردن ورودیها/کلاینتها،
|
||||||
کلاینتهای آنلاین، «بهزودی تمامشونده» و یک جادوگر کامل **افزودن کلاینت** را در اختیار دارند.
|
کلاینتهای آنلاین، «بهزودی تمامشونده» و یک جادوگر کامل **افزودن کلاینت** را در اختیار دارند.
|
||||||
|
کارت **کلاینت** در اختیار ادمین است و لینک اشتراک، لینکهای منفرد و کد QR کلاینت را
|
||||||
|
نشان میدهد، تا کانفیگ را بتوان به کلاینتی که هنوز به Telegram دسترسی ندارد هم داد.
|
||||||
کاربران عادی دکمههایی برای مصرف خودشان، لینکهای اشتراک، لینکهای منفرد و کدهای QR دارند.
|
کاربران عادی دکمههایی برای مصرف خودشان، لینکهای اشتراک، لینکهای منفرد و کدهای QR دارند.
|
||||||
|
|
||||||
## گزارشها و هشدارها
|
## گزارشها و هشدارها
|
||||||
|
|||||||
@@ -80,9 +80,11 @@ ID темы после двоеточия, например `-1001234567890:42`.
|
|||||||
Администраторам также доступны сценарии со встроенными кнопками: использование
|
Администраторам также доступны сценарии со встроенными кнопками: использование
|
||||||
сервера, отсортированные отчёты по трафику, сброс трафика, резервные копии БД,
|
сервера, отсортированные отчёты по трафику, сброс трафика, резервные копии БД,
|
||||||
журналы блокировок, список входящих подключений/клиентов, онлайн-клиенты,
|
журналы блокировок, список входящих подключений/клиентов, онлайн-клиенты,
|
||||||
«скоро исчерпают» и полноценный мастер **добавления клиента**. Обычным
|
«скоро исчерпают» и полноценный мастер **добавления клиента**. В карточке
|
||||||
пользователям доступны кнопки для своего расхода, ссылок на подписку, отдельных
|
клиента администратор получает ссылку на подписку, отдельные ссылки и QR-код —
|
||||||
ссылок и QR-кодов.
|
конфигурацию можно передать клиенту, который ещё не может открыть Telegram.
|
||||||
|
Обычным пользователям доступны кнопки для своего расхода, ссылок на подписку,
|
||||||
|
отдельных ссылок и QR-кодов.
|
||||||
|
|
||||||
## Отчёты и оповещения
|
## Отчёты и оповещения
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ ID 的所有客户端。订阅 ID 就是邀请码,因此请保持其足够长
|
|||||||
|
|
||||||
管理员还可通过内联按钮使用一系列功能:服务器用量、按流量排序的报告、
|
管理员还可通过内联按钮使用一系列功能:服务器用量、按流量排序的报告、
|
||||||
重置流量、数据库备份、封禁日志、列出入站/客户端、在线客户端、
|
重置流量、数据库备份、封禁日志、列出入站/客户端、在线客户端、
|
||||||
“即将耗尽”,以及完整的**添加客户端**向导。普通用户则可以使用按钮查看
|
“即将耗尽”,以及完整的**添加客户端**向导。管理员的**客户端卡片**上还带有该客户端的
|
||||||
|
订阅链接、单条链接和 QR 码,因此可以把配置交给暂时还无法打开 Telegram 的客户端。
|
||||||
|
普通用户则可以使用按钮查看
|
||||||
自己的用量、订阅链接、单条链接和 QR 码。
|
自己的用量、订阅链接、单条链接和 QR 码。
|
||||||
|
|
||||||
## 报告与告警
|
## 报告与告警
|
||||||
|
|||||||
@@ -140,6 +140,12 @@ func (t *Tgbot) clientRows(email string) [][]telego.InlineKeyboardButton {
|
|||||||
t.btn("tgbot.buttons.setTGUser", t.encodeQuery("tg_user "+email)),
|
t.btn("tgbot.buttons.setTGUser", t.encodeQuery("tg_user "+email)),
|
||||||
t.btn("tgbot.buttons.inviteLink", t.encodeQuery("client_invite_link "+email)),
|
t.btn("tgbot.buttons.inviteLink", t.encodeQuery("client_invite_link "+email)),
|
||||||
),
|
),
|
||||||
|
// An invite link needs the client to reach Telegram; a subscription URL
|
||||||
|
// and a QR code do not, so they are the admin's way to hand one over.
|
||||||
|
tu.InlineKeyboardRow(
|
||||||
|
t.btn("tgbot.buttons.links", t.encodeQuery("client_sub_links "+email)),
|
||||||
|
t.btn("tgbot.buttons.qrCode", t.encodeQuery("client_qr_links "+email)),
|
||||||
|
),
|
||||||
tu.InlineKeyboardRow(t.btn("tgbot.buttons.toggle", t.encodeQuery("toggle_enable "+email))),
|
tu.InlineKeyboardRow(t.btn("tgbot.buttons.toggle", t.encodeQuery("toggle_enable "+email))),
|
||||||
t.backRow(),
|
t.backRow(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1166,3 +1166,145 @@ func TestLinksScreenShowsUsageToItsOwner(t *testing.T) {
|
|||||||
t.Errorf("a client is offered the admin's client card: %s", markup)
|
t.Errorf("a client is offered the admin's client card: %s", markup)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
adminTgID = int64(31337)
|
||||||
|
adminCardEmail = "admin-card@example.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
// seedAdminClient stores one client the way the panel keeps it: an inbound row
|
||||||
|
// with its settings blob, a traffic row and a client record.
|
||||||
|
func seedAdminClient(t *testing.T, email string) {
|
||||||
|
t.Helper()
|
||||||
|
inbound := &model.Inbound{
|
||||||
|
UserId: 1, Tag: "in-" + email, Remark: "in-" + email, Port: 8443,
|
||||||
|
Protocol: model.VLESS, Enable: true,
|
||||||
|
Settings: `{"clients":[{"email":"` + email + `","enable":true,"subId":"sub-` + email + `"}]}`,
|
||||||
|
}
|
||||||
|
if err := database.GetDB().Create(inbound).Error; err != nil {
|
||||||
|
t.Fatalf("seed inbound: %v", err)
|
||||||
|
}
|
||||||
|
if err := database.GetDB().Create(&xray.ClientTraffic{
|
||||||
|
InboundId: inbound.Id, Email: email, Enable: true,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("seed traffic: %v", err)
|
||||||
|
}
|
||||||
|
record := (&model.Client{Email: email, Enable: true, SubID: "sub-" + email}).ToRecord()
|
||||||
|
if err := database.GetDB().Create(record).Error; err != nil {
|
||||||
|
t.Fatalf("seed client: %v", err)
|
||||||
|
}
|
||||||
|
if err := database.GetDB().Create(&model.ClientInbound{ClientId: record.Id, InboundId: inbound.Id}).Error; err != nil {
|
||||||
|
t.Fatalf("seed client_inbounds: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cardCallback finds the callback data of the card's button for one action.
|
||||||
|
func cardCallback(t *testing.T, sc *screen, action string) string {
|
||||||
|
t.Helper()
|
||||||
|
for _, row := range sc.rows {
|
||||||
|
for _, btn := range row {
|
||||||
|
if strings.HasPrefix(btn.CallbackData, action+" ") {
|
||||||
|
return btn.CallbackData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("the client card offers no %q button: %+v", action, sc.rows)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// The v3.11.0 rebuild left the admin's client card with no way to the subscription link or
|
||||||
|
// QR code, so an admin could not hand a config to a client who cannot reach Telegram (#6984).
|
||||||
|
func TestAdminClientCardOffersTheLinksAndQRCode(t *testing.T) {
|
||||||
|
tb, _ := newScreenTgbot(t, false)
|
||||||
|
initReportDB(t)
|
||||||
|
setTestAdmins(t, adminTgID)
|
||||||
|
seedAdminClient(t, adminCardEmail)
|
||||||
|
|
||||||
|
tb.searchClientScreen(adminTgID, adminCardEmail)
|
||||||
|
|
||||||
|
sc, ok := tb.screens().get(adminTgID)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("no screen was tracked")
|
||||||
|
}
|
||||||
|
markup, _ := json.Marshal(sc.markup)
|
||||||
|
for _, want := range []string{"client_sub_links " + adminCardEmail, "client_qr_links " + adminCardEmail} {
|
||||||
|
if !strings.Contains(string(markup), want) {
|
||||||
|
t.Errorf("the admin's client card lacks %q: %s", want, markup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The button has to work, not just exist: tapping it must draw the subscription screen,
|
||||||
|
// since a URL the admin can read is the whole point of the flow.
|
||||||
|
func TestAdminTappingTheClientCardLinksDrawsTheSubscription(t *testing.T) {
|
||||||
|
tb, calls := newScreenTgbot(t, false)
|
||||||
|
initReportDB(t)
|
||||||
|
localizeWithRealBundle(t)
|
||||||
|
setTestAdmins(t, adminTgID)
|
||||||
|
seedAdminClient(t, adminCardEmail)
|
||||||
|
|
||||||
|
tb.searchClientScreen(adminTgID, adminCardEmail)
|
||||||
|
sc, ok := tb.screens().get(adminTgID)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("no screen was tracked")
|
||||||
|
}
|
||||||
|
data := cardCallback(t, sc, "client_sub_links")
|
||||||
|
|
||||||
|
tb.answerCallback(&telego.CallbackQuery{
|
||||||
|
ID: "q1",
|
||||||
|
From: telego.User{ID: adminTgID},
|
||||||
|
Data: data,
|
||||||
|
Message: &telego.Message{Chat: telego.Chat{ID: adminTgID}},
|
||||||
|
}, true)
|
||||||
|
|
||||||
|
links, ok := tb.screens().get(adminTgID)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("no screen was tracked")
|
||||||
|
}
|
||||||
|
if links.kind != "links" {
|
||||||
|
t.Fatalf("screen kind = %q, want the links screen", links.kind)
|
||||||
|
}
|
||||||
|
if !strings.Contains(links.text, "sub-") {
|
||||||
|
t.Errorf("the links screen carries no subscription url: %q", links.text)
|
||||||
|
}
|
||||||
|
if countMethod(calls(), "editMessageMedia")+countMethod(calls(), "editMessageText") == 0 {
|
||||||
|
t.Error("the links screen was never drawn")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The QR code is the second half of the request: an admin sends a picture
|
||||||
|
// straight to a client, so the card must reach it too.
|
||||||
|
func TestAdminTappingTheClientCardQRCodeDrawsTheCode(t *testing.T) {
|
||||||
|
tb, calls := newScreenTgbot(t, false)
|
||||||
|
initReportDB(t)
|
||||||
|
setTestAdmins(t, adminTgID)
|
||||||
|
seedAdminClient(t, adminCardEmail)
|
||||||
|
|
||||||
|
tb.searchClientScreen(adminTgID, adminCardEmail)
|
||||||
|
sc, ok := tb.screens().get(adminTgID)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("no screen was tracked")
|
||||||
|
}
|
||||||
|
data := cardCallback(t, sc, "client_qr_links")
|
||||||
|
|
||||||
|
tb.answerCallback(&telego.CallbackQuery{
|
||||||
|
ID: "q1",
|
||||||
|
From: telego.User{ID: adminTgID},
|
||||||
|
Data: data,
|
||||||
|
Message: &telego.Message{Chat: telego.Chat{ID: adminTgID}},
|
||||||
|
}, true)
|
||||||
|
|
||||||
|
qr, ok := tb.screens().get(adminTgID)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("no screen was tracked")
|
||||||
|
}
|
||||||
|
if qr.kind != "qr" {
|
||||||
|
t.Fatalf("screen kind = %q, want the qr screen", qr.kind)
|
||||||
|
}
|
||||||
|
if qr.qrPicture == nil {
|
||||||
|
t.Error("the qr screen carries no sheet of codes")
|
||||||
|
}
|
||||||
|
if countMethod(calls(), "sendPhoto") == 0 && countMethod(calls(), "editMessageMedia") == 0 {
|
||||||
|
t.Error("the qr screen was never drawn")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user