From 2928b52b045e9327378e60aef23af4739f5f8f8c Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Fri, 15 May 2026 13:12:54 +0200 Subject: [PATCH] feat(tgbot): add Flow picker when creating a VLESS client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- web/service/tgbot.go | 74 +++++++++++++++++++++++++++++++++++++- web/translation/ar-EG.json | 1 + web/translation/en-US.json | 1 + web/translation/es-ES.json | 1 + web/translation/fa-IR.json | 1 + web/translation/id-ID.json | 1 + web/translation/ja-JP.json | 1 + web/translation/pt-BR.json | 1 + web/translation/ru-RU.json | 1 + web/translation/tr-TR.json | 1 + web/translation/uk-UA.json | 1 + web/translation/vi-VN.json | 1 + web/translation/zh-CN.json | 1 + web/translation/zh-TW.json | 1 + 14 files changed, 86 insertions(+), 1 deletion(-) diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 3d737c706..179c082fe 100644 --- a/web/service/tgbot.go +++ b/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( diff --git a/web/translation/ar-EG.json b/web/translation/ar-EG.json index 663e81f38..ecf1c19d3 100644 --- a/web/translation/ar-EG.json +++ b/web/translation/ar-EG.json @@ -952,6 +952,7 @@ "change_password": "⚙️🔑 كلمة السر", "change_email": "⚙️📧 البريد الإلكتروني", "change_comment": "⚙️💬 تعليق", + "change_flow": "⚙️🚦 التدفق", "ResetAllTraffics": "إعادة ضبط جميع الترافيك", "SortedTrafficUsageReport": "تقرير استخدام الترافيك المرتب" }, diff --git a/web/translation/en-US.json b/web/translation/en-US.json index 5cf1f7c02..eedd34545 100644 --- a/web/translation/en-US.json +++ b/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" }, diff --git a/web/translation/es-ES.json b/web/translation/es-ES.json index 9b6a5e87f..34af89caf 100644 --- a/web/translation/es-ES.json +++ b/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" }, diff --git a/web/translation/fa-IR.json b/web/translation/fa-IR.json index 1295e400c..fd9487885 100644 --- a/web/translation/fa-IR.json +++ b/web/translation/fa-IR.json @@ -952,6 +952,7 @@ "change_password": "⚙️🔑 گذرواژه", "change_email": "⚙️📧 ایمیل", "change_comment": "⚙️💬 نظر", + "change_flow": "⚙️🚦 جریان", "ResetAllTraffics": "بازنشانی همه ترافیک‌ها", "SortedTrafficUsageReport": "گزارش استفاده از ترافیک مرتب‌شده" }, diff --git a/web/translation/id-ID.json b/web/translation/id-ID.json index 84769a3bb..b8f44ac61 100644 --- a/web/translation/id-ID.json +++ b/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" }, diff --git a/web/translation/ja-JP.json b/web/translation/ja-JP.json index 913ef5e55..630f5623a 100644 --- a/web/translation/ja-JP.json +++ b/web/translation/ja-JP.json @@ -952,6 +952,7 @@ "change_password": "⚙️🔑 パスワード", "change_email": "⚙️📧 メールアドレス", "change_comment": "⚙️💬 コメント", + "change_flow": "⚙️🚦 フロー", "ResetAllTraffics": "すべてのトラフィックをリセット", "SortedTrafficUsageReport": "ソートされたトラフィック使用レポート" }, diff --git a/web/translation/pt-BR.json b/web/translation/pt-BR.json index 74cd6ba75..9f1b67d74 100644 --- a/web/translation/pt-BR.json +++ b/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" }, diff --git a/web/translation/ru-RU.json b/web/translation/ru-RU.json index 3c16ede8d..f7ddafa61 100644 --- a/web/translation/ru-RU.json +++ b/web/translation/ru-RU.json @@ -952,6 +952,7 @@ "change_password": "⚙️🔑 Пароль", "change_email": "⚙️📧 Email", "change_comment": "⚙️💬 Комментарий", + "change_flow": "⚙️🚦 Поток", "ResetAllTraffics": "Сбросить весь трафик", "SortedTrafficUsageReport": "Отсортированный отчет об использовании трафика" }, diff --git a/web/translation/tr-TR.json b/web/translation/tr-TR.json index 7895c1b60..a8dc2c3c0 100644 --- a/web/translation/tr-TR.json +++ b/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" }, diff --git a/web/translation/uk-UA.json b/web/translation/uk-UA.json index 6ed76eb07..417866d1d 100644 --- a/web/translation/uk-UA.json +++ b/web/translation/uk-UA.json @@ -952,6 +952,7 @@ "change_password": "⚙️🔑 Пароль", "change_email": "⚙️📧 Електронна пошта", "change_comment": "⚙️💬 Коментар", + "change_flow": "⚙️🚦 Потік", "ResetAllTraffics": "Скинути весь трафік", "SortedTrafficUsageReport": "Відсортований звіт про використання трафіку" }, diff --git a/web/translation/vi-VN.json b/web/translation/vi-VN.json index 09e8d8b24..b292c0587 100644 --- a/web/translation/vi-VN.json +++ b/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" }, diff --git a/web/translation/zh-CN.json b/web/translation/zh-CN.json index 0d65cb53b..3f648cb2c 100644 --- a/web/translation/zh-CN.json +++ b/web/translation/zh-CN.json @@ -952,6 +952,7 @@ "change_password": "⚙️🔑 密码", "change_email": "⚙️📧 邮箱", "change_comment": "⚙️💬 评论", + "change_flow": "⚙️🚦 流控", "ResetAllTraffics": "重置所有流量", "SortedTrafficUsageReport": "排序的流量使用报告" }, diff --git a/web/translation/zh-TW.json b/web/translation/zh-TW.json index 56a0c0752..a28c0e0f8 100644 --- a/web/translation/zh-TW.json +++ b/web/translation/zh-TW.json @@ -952,6 +952,7 @@ "change_password": "⚙️🔑 密碼", "change_email": "⚙️📧 電子郵件", "change_comment": "⚙️💬 評論", + "change_flow": "⚙️🚦 流控", "ResetAllTraffics": "重設所有流量", "SortedTrafficUsageReport": "排序過的流量使用報告" },