mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-30 03:02:08 +03:00
fix(tgbot): unstick the add-client wizard's inbound picker (#6621)
* fix(tgbot): unstick the add-client wizard's inbound picker Tapping "➕ Новый клиент" always sent the "choose inbound" message, even when the button list ended up empty after protocol filtering. getInboundsAddClient checked len(inbounds)==0 before filtering but never re-checked after, so an admin whose every inbound was excluded got a message with nothing to tap and no further feedback. WireGuard and AmneziaWG were excluded outright too, a holdover from the wizard's original 2025 implementation, before defaultWireguardClients and defaultAmneziaWGClients existed. Both now auto-generate a keypair + AllowedIPs for a client with none set, and the subscription server already emits wireguard:// and vpn:// share links for them, so both inbound types flow through the same generic Create path as VLESS/Trojan already used by the bot. Mixed/HTTP/Tunnel stay excluded: they have no per-client model in this codebase. - getInboundsAddClient now returns getInboundsFailed when the button list is empty after filtering, instead of sending an unusable keyboard - WireGuard/AmneziaWG removed from the exclusion list in both getInboundsAddClient and getInboundsAttachPicker - the previously duplicated excludedProtocols map is now a single package-level addClientExcludedProtocols shared by both functions * test(tgbot): pin which inbounds the add-client picker offers The picker change had no test. One drives a database holding WireGuard, AmneziaWG, VLESS and Mixed inbounds and wants the first three offered; the other holds only Mixed, HTTP and Tunnel and wants getInboundsFailed instead of an empty keyboard. Both fail on the previous picker. --------- Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package tgbot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
|
||||
"github.com/mymmrac/telego"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
)
|
||||
|
||||
func seedPickerInbounds(t *testing.T, protocols ...model.Protocol) {
|
||||
t.Helper()
|
||||
if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
|
||||
t.Fatalf("InitDB: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.CloseDB() })
|
||||
for i, protocol := range protocols {
|
||||
port := 20000 + i
|
||||
ib := &model.Inbound{Remark: string(protocol), Enable: true, Port: port, Protocol: protocol, Tag: fmt.Sprintf("inbound-%d", port), Settings: `{}`}
|
||||
if err := database.GetDB().Create(ib).Error; err != nil {
|
||||
t.Fatalf("seed %s inbound: %v", protocol, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pickerLabels(keyboard *telego.InlineKeyboardMarkup) []string {
|
||||
var labels []string
|
||||
for _, row := range keyboard.InlineKeyboard {
|
||||
for _, button := range row {
|
||||
labels = append(labels, button.Text)
|
||||
}
|
||||
}
|
||||
slices.Sort(labels)
|
||||
return labels
|
||||
}
|
||||
|
||||
// WireGuard and AmneziaWG clients get a generated keypair and address on Create,
|
||||
// so the wizard offers them; Mixed authenticates per inbound and has no clients.
|
||||
func TestAddClientPickerOffersWireGuardAndAmneziaWG(t *testing.T) {
|
||||
seedPickerInbounds(t, model.VLESS, model.WireGuard, model.AmneziaWG, model.Mixed)
|
||||
|
||||
keyboard, err := (&Tgbot{}).getInboundsAddClient()
|
||||
if err != nil {
|
||||
t.Fatalf("getInboundsAddClient: %v", err)
|
||||
}
|
||||
want := []string{"amneziawg - ✅", "vless - ✅", "wireguard - ✅"}
|
||||
if got := pickerLabels(keyboard); !slices.Equal(got, want) {
|
||||
t.Fatalf("picker buttons = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// An empty keyboard sent the admin a "choose inbound" prompt with nothing to tap.
|
||||
func TestAddClientPickerFailsWhenNoInboundTakesClients(t *testing.T) {
|
||||
draftLocalizer(t, &i18n.Message{ID: "tgbot.answers.getInboundsFailed", Other: "Failed to get inbounds."})
|
||||
seedPickerInbounds(t, model.Mixed, model.HTTP, model.Tunnel)
|
||||
|
||||
keyboard, err := (&Tgbot{}).getInboundsAddClient()
|
||||
if err == nil || err.Error() != "Failed to get inbounds." {
|
||||
t.Fatalf("getInboundsAddClient = (%v, %v), want the getInboundsFailed error", keyboard, err)
|
||||
}
|
||||
}
|
||||
@@ -136,6 +136,14 @@ func (t *Tgbot) getInboundClientsFor(inbound *model.Inbound, action string) (*te
|
||||
return keyboard, nil
|
||||
}
|
||||
|
||||
// addClientExcludedProtocols are the protocols with no per-client model: Tunnel
|
||||
// has no clients, Mixed/HTTP authenticate at the inbound level, not per-client.
|
||||
var addClientExcludedProtocols = map[model.Protocol]bool{
|
||||
model.Tunnel: true,
|
||||
model.Mixed: true,
|
||||
model.HTTP: true,
|
||||
}
|
||||
|
||||
// getInboundsAddClient creates an inline keyboard for adding clients to inbounds.
|
||||
func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
|
||||
inbounds, err := t.inboundService.GetAllInbounds()
|
||||
@@ -149,17 +157,9 @@ func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
|
||||
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
|
||||
}
|
||||
|
||||
excludedProtocols := map[model.Protocol]bool{
|
||||
model.Tunnel: true,
|
||||
model.Mixed: true,
|
||||
model.WireGuard: true,
|
||||
model.AmneziaWG: true,
|
||||
model.HTTP: true,
|
||||
}
|
||||
|
||||
var buttons []telego.InlineKeyboardButton
|
||||
for _, inbound := range inbounds {
|
||||
if excludedProtocols[inbound.Protocol] {
|
||||
if addClientExcludedProtocols[inbound.Protocol] {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -171,6 +171,11 @@ func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
|
||||
buttons = append(buttons, tu.InlineKeyboardButton(fmt.Sprintf("%v - %v", inbound.Remark, status)).WithCallbackData(callbackData))
|
||||
}
|
||||
|
||||
if len(buttons) == 0 {
|
||||
logger.Warning("No inbounds eligible for add-client (all excluded by protocol)")
|
||||
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
|
||||
}
|
||||
|
||||
cols := 1
|
||||
if len(buttons) >= 6 {
|
||||
cols = 2
|
||||
@@ -194,20 +199,13 @@ func (t *Tgbot) getInboundsAttachPicker(draft *clientDraft) (*telego.InlineKeybo
|
||||
if len(inbounds) == 0 {
|
||||
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
|
||||
}
|
||||
excludedProtocols := map[model.Protocol]bool{
|
||||
model.Tunnel: true,
|
||||
model.Mixed: true,
|
||||
model.WireGuard: true,
|
||||
model.AmneziaWG: true,
|
||||
model.HTTP: true,
|
||||
}
|
||||
selected := make(map[int]bool, len(draft.receiverInboundIDs))
|
||||
for _, id := range draft.receiverInboundIDs {
|
||||
selected[id] = true
|
||||
}
|
||||
var buttons []telego.InlineKeyboardButton
|
||||
for _, ib := range inbounds {
|
||||
if excludedProtocols[ib.Protocol] {
|
||||
if addClientExcludedProtocols[ib.Protocol] {
|
||||
continue
|
||||
}
|
||||
mark := "☐"
|
||||
|
||||
Reference in New Issue
Block a user