Browse Source

feat(tgbot): add Flow picker when creating a VLESS client

The bot's add-client flow already serialised client_Flow into the VLESS
JSON template but never exposed a way to set it from Telegram, so every
client ended up with an empty flow regardless of the inbound's transport.

Added an inline "Flow" row to the VLESS protocol keyboard with three
choices — None, xtls-rprx-vision, and xtls-rprx-vision-udp443 — and a
matching i18n key in all 13 locale files. The row is only shown when
the inbound can actually use Vision flow (mirrors the frontend's
canEnableTlsFlow check: VLESS over TCP with TLS or Reality); on other
transports it's hidden and any stale client_Flow value is reset, so the
generated JSON stays consistent with the inbound's stream settings.
MHSanaei 12 hours ago
parent
commit
2928b52b04

+ 73 - 1
web/service/tgbot.go

@@ -1398,6 +1398,25 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 					return
 				}
 
+				t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
+				t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
+			case "add_client_set_flow":
+				if dataArray[1] == "none" {
+					client_Flow = ""
+				} else {
+					client_Flow = dataArray[1]
+				}
+				messageId := callbackQuery.Message.GetMessageID()
+				inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
+				if err != nil {
+					t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
+					return
+				}
+				message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
+				if err != nil {
+					t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
+					return
+				}
 				t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
 				t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
 			case "add_client_ip_limit_in":
@@ -1865,6 +1884,22 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
 			),
 		)
 		t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
+	case "add_client_ch_default_flow":
+		inlineKeyboard := tu.InlineKeyboard(
+			tu.InlineKeyboardRow(
+				tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("add_client_default_traffic_exp")),
+			),
+			tu.InlineKeyboardRow(
+				tu.InlineKeyboardButton("None").WithCallbackData(t.encodeQuery("add_client_set_flow none")),
+			),
+			tu.InlineKeyboardRow(
+				tu.InlineKeyboardButton("xtls-rprx-vision").WithCallbackData(t.encodeQuery("add_client_set_flow xtls-rprx-vision")),
+			),
+			tu.InlineKeyboardRow(
+				tu.InlineKeyboardButton("xtls-rprx-vision-udp443").WithCallbackData(t.encodeQuery("add_client_set_flow xtls-rprx-vision-udp443")),
+			),
+		)
+		t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
 	case "add_client_ch_default_ip_limit":
 		inlineKeyboard := tu.InlineKeyboard(
 			tu.InlineKeyboardRow(
@@ -3345,6 +3380,25 @@ func (t *Tgbot) getCommonClientButtons() [][]telego.InlineKeyboardButton {
 	}
 }
 
+// inboundCanEnableTlsFlow mirrors Inbound.canEnableTlsFlow() from the frontend
+// model: xtls-rprx-vision is only valid on VLESS-over-TCP with TLS or Reality.
+func inboundCanEnableTlsFlow(ib *model.Inbound) bool {
+	if ib == nil || ib.Protocol != model.VLESS {
+		return false
+	}
+	var stream struct {
+		Network  string `json:"network"`
+		Security string `json:"security"`
+	}
+	if err := json.Unmarshal([]byte(ib.StreamSettings), &stream); err != nil {
+		return false
+	}
+	if stream.Network != "tcp" {
+		return false
+	}
+	return stream.Security == "tls" || stream.Security == "reality"
+}
+
 // addClient handles the process of adding a new client to an inbound.
 func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
 	inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
@@ -3357,13 +3411,31 @@ func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
 
 	var protocolRows [][]telego.InlineKeyboardButton
 	switch protocol {
-	case model.VMESS, model.VLESS:
+	case model.VMESS:
 		protocolRows = [][]telego.InlineKeyboardButton{
 			tu.InlineKeyboardRow(
 				tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
 				tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_id")).WithCallbackData("add_client_ch_default_id"),
 			),
 		}
+	case model.VLESS:
+		protocolRows = [][]telego.InlineKeyboardButton{
+			tu.InlineKeyboardRow(
+				tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
+				tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_id")).WithCallbackData("add_client_ch_default_id"),
+			),
+		}
+		if inboundCanEnableTlsFlow(inbound) {
+			flowLabel := t.I18nBot("tgbot.buttons.change_flow")
+			if client_Flow != "" {
+				flowLabel = flowLabel + ": " + client_Flow
+			}
+			protocolRows = append(protocolRows, tu.InlineKeyboardRow(
+				tu.InlineKeyboardButton(flowLabel).WithCallbackData("add_client_ch_default_flow"),
+			))
+		} else if client_Flow != "" {
+			client_Flow = ""
+		}
 	case model.Trojan:
 		protocolRows = [][]telego.InlineKeyboardButton{
 			tu.InlineKeyboardRow(

+ 1 - 0
web/translation/ar-EG.json

@@ -952,6 +952,7 @@
       "change_password": "⚙️🔑 كلمة السر",
       "change_email": "⚙️📧 البريد الإلكتروني",
       "change_comment": "⚙️💬 تعليق",
+      "change_flow": "⚙️🚦 التدفق",
       "ResetAllTraffics": "إعادة ضبط جميع الترافيك",
       "SortedTrafficUsageReport": "تقرير استخدام الترافيك المرتب"
     },

+ 1 - 0
web/translation/en-US.json

@@ -952,6 +952,7 @@
       "change_password": "⚙️🔑 Password",
       "change_email": "⚙️📧 Email",
       "change_comment": "⚙️💬 Comment",
+      "change_flow": "⚙️🚦 Flow",
       "ResetAllTraffics": "Reset All Traffics",
       "SortedTrafficUsageReport": "Sorted Traffic Usage Report"
     },

+ 1 - 0
web/translation/es-ES.json

@@ -952,6 +952,7 @@
       "change_password": "⚙️🔑 Contraseña",
       "change_email": "⚙️📧 Correo electrónico",
       "change_comment": "⚙️💬 Comentario",
+      "change_flow": "⚙️🚦 Flujo",
       "ResetAllTraffics": "Reiniciar todo el tráfico",
       "SortedTrafficUsageReport": "Informe de uso de tráfico ordenado"
     },

+ 1 - 0
web/translation/fa-IR.json

@@ -952,6 +952,7 @@
       "change_password": "⚙️🔑 گذرواژه",
       "change_email": "⚙️📧 ایمیل",
       "change_comment": "⚙️💬 نظر",
+      "change_flow": "⚙️🚦 جریان",
       "ResetAllTraffics": "بازنشانی همه ترافیک‌ها",
       "SortedTrafficUsageReport": "گزارش استفاده از ترافیک مرتب‌شده"
     },

+ 1 - 0
web/translation/id-ID.json

@@ -952,6 +952,7 @@
       "change_password": "⚙️🔑 Kata Sandi",
       "change_email": "⚙️📧 Email",
       "change_comment": "⚙️💬 Komentar",
+      "change_flow": "⚙️🚦 Flow",
       "ResetAllTraffics": "Reset Semua Lalu Lintas",
       "SortedTrafficUsageReport": "Laporan Penggunaan Lalu Lintas yang Terurut"
     },

+ 1 - 0
web/translation/ja-JP.json

@@ -952,6 +952,7 @@
       "change_password": "⚙️🔑 パスワード",
       "change_email": "⚙️📧 メールアドレス",
       "change_comment": "⚙️💬 コメント",
+      "change_flow": "⚙️🚦 フロー",
       "ResetAllTraffics": "すべてのトラフィックをリセット",
       "SortedTrafficUsageReport": "ソートされたトラフィック使用レポート"
     },

+ 1 - 0
web/translation/pt-BR.json

@@ -952,6 +952,7 @@
       "change_password": "⚙️🔑 Senha",
       "change_email": "⚙️📧 E-mail",
       "change_comment": "⚙️💬 Comentário",
+      "change_flow": "⚙️🚦 Fluxo",
       "ResetAllTraffics": "Redefinir Todo o Tráfego",
       "SortedTrafficUsageReport": "Relatório de Uso de Tráfego Ordenado"
     },

+ 1 - 0
web/translation/ru-RU.json

@@ -952,6 +952,7 @@
       "change_password": "⚙️🔑 Пароль",
       "change_email": "⚙️📧 Email",
       "change_comment": "⚙️💬 Комментарий",
+      "change_flow": "⚙️🚦 Поток",
       "ResetAllTraffics": "Сбросить весь трафик",
       "SortedTrafficUsageReport": "Отсортированный отчет об использовании трафика"
     },

+ 1 - 0
web/translation/tr-TR.json

@@ -952,6 +952,7 @@
       "change_password": "⚙️🔑 Şifre",
       "change_email": "⚙️📧 E-posta",
       "change_comment": "⚙️💬 Yorum",
+      "change_flow": "⚙️🚦 Akış",
       "ResetAllTraffics": "Tüm Trafikleri Sıfırla",
       "SortedTrafficUsageReport": "Sıralı Trafik Kullanım Raporu"
     },

+ 1 - 0
web/translation/uk-UA.json

@@ -952,6 +952,7 @@
       "change_password": "⚙️🔑 Пароль",
       "change_email": "⚙️📧 Електронна пошта",
       "change_comment": "⚙️💬 Коментар",
+      "change_flow": "⚙️🚦 Потік",
       "ResetAllTraffics": "Скинути весь трафік",
       "SortedTrafficUsageReport": "Відсортований звіт про використання трафіку"
     },

+ 1 - 0
web/translation/vi-VN.json

@@ -952,6 +952,7 @@
       "change_password": "⚙️🔑 Mật Khẩu",
       "change_email": "⚙️📧 Email",
       "change_comment": "⚙️💬 Bình Luận",
+      "change_flow": "⚙️🚦 Flow",
       "ResetAllTraffics": "Đặt lại tất cả lưu lượng",
       "SortedTrafficUsageReport": "Báo cáo sử dụng lưu lượng đã sắp xếp"
     },

+ 1 - 0
web/translation/zh-CN.json

@@ -952,6 +952,7 @@
       "change_password": "⚙️🔑 密码",
       "change_email": "⚙️📧 邮箱",
       "change_comment": "⚙️💬 评论",
+      "change_flow": "⚙️🚦 流控",
       "ResetAllTraffics": "重置所有流量",
       "SortedTrafficUsageReport": "排序的流量使用报告"
     },

+ 1 - 0
web/translation/zh-TW.json

@@ -952,6 +952,7 @@
       "change_password": "⚙️🔑 密碼",
       "change_email": "⚙️📧 電子郵件",
       "change_comment": "⚙️💬 評論",
+      "change_flow": "⚙️🚦 流控",
       "ResetAllTraffics": "重設所有流量",
       "SortedTrafficUsageReport": "排序過的流量使用報告"
     },