feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options

- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
  multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
  let an admin pick one or more inbounds and submit a single client; per-
  protocol secrets are now generated server-side via fillProtocolDefaults.
  Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
  and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
  setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
  during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
  Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
  still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
  {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
  pickers so the dropdown payload stays small on panels with thousands of
  clients (drops settings JSON, clientStats, streamSettings). Server-side
  TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
  needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
  reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
  clients add/update bodies.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MHSanaei 2026-05-18 02:11:30 +02:00
parent 14ad255c38
commit d10fa8f3c0
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
7 changed files with 401 additions and 515 deletions

View file

@ -80,6 +80,13 @@ export const sections = [
response: response:
'{\n "success": true,\n "obj": [\n {\n "id": 1,\n "userId": 1,\n "up": 0,\n "down": 0,\n "total": 0,\n "remark": "VLESS-443",\n "enable": true,\n "expiryTime": 0,\n "listen": "",\n "port": 443,\n "protocol": "vless",\n "settings": {\n "clients": [],\n "decryption": "none"\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": { "show": false, "dest": "..." }\n },\n "tag": "inbound-443",\n "sniffing": {\n "enabled": true,\n "destOverride": ["http", "tls"]\n },\n "clientStats": []\n }\n ]\n}', '{\n "success": true,\n "obj": [\n {\n "id": 1,\n "userId": 1,\n "up": 0,\n "down": 0,\n "total": 0,\n "remark": "VLESS-443",\n "enable": true,\n "expiryTime": 0,\n "listen": "",\n "port": 443,\n "protocol": "vless",\n "settings": {\n "clients": [],\n "decryption": "none"\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": { "show": false, "dest": "..." }\n },\n "tag": "inbound-443",\n "sniffing": {\n "enabled": true,\n "destOverride": ["http", "tls"]\n },\n "clientStats": []\n }\n ]\n}',
}, },
{
method: 'GET',
path: '/panel/api/inbounds/options',
summary: 'Lightweight picker projection of the authenticated users inbounds. Returns only id, remark, protocol, port, and a server-computed tlsFlowCapable flag (true for VLESS / port-fallback on TCP with tls or reality). Use this for dropdowns and attach pickers — it skips settings, streamSettings, and clientStats so the payload stays small even on panels with thousands of clients.',
response:
'{\n "success": true,\n "obj": [\n {\n "id": 1,\n "remark": "VLESS-443",\n "protocol": "vless",\n "port": 443,\n "tlsFlowCapable": true\n }\n ]\n}',
},
{ {
method: 'GET', method: 'GET',
path: '/panel/api/inbounds/get/:id', path: '/panel/api/inbounds/get/:id',
@ -392,22 +399,22 @@ export const sections = [
{ {
method: 'POST', method: 'POST',
path: '/panel/api/clients/add', path: '/panel/api/clients/add',
summary: 'Create a new client and attach it to one or more inbounds in a single call. Body is JSON.', summary: 'Create a new client and attach it to one or more inbounds in a single call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess, password for Trojan/Shadowsocks, auth for Hysteria) are generated server-side when omitted, so callers can send only the universal fields.',
params: [ params: [
{ name: 'client', in: 'body (json)', type: 'object', desc: 'Client fields: email, subId, id (uuid), password, auth, totalGB, expiryTime, limitIp, comment, enable.' }, { name: 'client', in: 'body (json)', type: 'object', desc: 'Client fields: email, subId, id (uuid), password, auth, flow, totalGB, expiryTime, limitIp, tgId (numeric Telegram user ID, 0 = none), comment, enable.' },
{ name: 'inboundIds', in: 'body (json)', type: 'integer[]', desc: 'Inbound IDs to attach the client to. At least one required.' }, { name: 'inboundIds', in: 'body (json)', type: 'integer[]', desc: 'Inbound IDs to attach the client to. At least one required.' },
], ],
body: '{\n "client": {\n "email": "alice@example.com",\n "totalGB": 53687091200,\n "expiryTime": 1735689600000\n },\n "inboundIds": [3, 5]\n}', body: '{\n "client": {\n "email": "alice@example.com",\n "totalGB": 53687091200,\n "expiryTime": 1735689600000,\n "tgId": 0,\n "limitIp": 0,\n "enable": true\n },\n "inboundIds": [3, 5]\n}',
response: '{\n "success": true,\n "msg": "Client added"\n}', response: '{\n "success": true,\n "msg": "Client added"\n}',
}, },
{ {
method: 'POST', method: 'POST',
path: '/panel/api/clients/update/:email', path: '/panel/api/clients/update/:email',
summary: 'Update an existing client by email. Changes propagate to every attached inbound. Body is the JSON client payload.', summary: 'Update an existing client by email. Changes propagate to every attached inbound. Body is the JSON client payload — supply the full set of fields you want to keep (the server replaces the row, it does not patch).',
params: [ params: [
{ name: 'email', in: 'path', type: 'string', desc: 'Current client email (unique identifier).' }, { name: 'email', in: 'path', type: 'string', desc: 'Current client email (unique identifier).' },
], ],
body: '{\n "email": "alice@example.com",\n "totalGB": 107374182400,\n "expiryTime": 1767225600000,\n "enable": true\n}', body: '{\n "email": "alice@example.com",\n "totalGB": 107374182400,\n "expiryTime": 1767225600000,\n "tgId": 123456789,\n "enable": true\n}',
response: '{\n "success": true,\n "msg": "Client updated"\n}', response: '{\n "success": true,\n "msg": "Client updated"\n}',
}, },
{ {

View file

@ -7,7 +7,6 @@ import { message } from 'ant-design-vue';
import { HttpUtil, RandomUtil, SizeFormatter } from '@/utils'; import { HttpUtil, RandomUtil, SizeFormatter } from '@/utils';
import DateTimePicker from '@/components/DateTimePicker.vue'; import DateTimePicker from '@/components/DateTimePicker.vue';
import { DBInbound } from '@/models/dbinbound.js';
import { TLS_FLOW_CONTROL } from '@/models/inbound.js'; import { TLS_FLOW_CONTROL } from '@/models/inbound.js';
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL); const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
@ -46,10 +45,7 @@ const form = reactive({
const flowCapableIds = computed(() => { const flowCapableIds = computed(() => {
const ids = new Set(); const ids = new Set();
for (const row of props.inbounds || []) { for (const row of props.inbounds || []) {
try { if (row?.tlsFlowCapable) ids.add(row.id);
const parsed = new DBInbound(row).toInbound();
if (parsed.canEnableTlsFlow?.()) ids.add(row.id);
} catch (_e) { /* ignore */ }
} }
return ids; return ids;
}); });
@ -176,9 +172,8 @@ async function submit() {
</script> </script>
<template> <template>
<a-modal :open="open" :title="t('pages.clients.bulk') || 'Add Bulk'" :ok-text="t('create')" <a-modal :open="open" :title="t('pages.clients.bulk') || 'Add Bulk'" :ok-text="t('create')" :cancel-text="t('close')"
:cancel-text="t('close')" :confirm-loading="saving" :mask-closable="false" :width="640" :confirm-loading="saving" :mask-closable="false" :width="640" @ok="submit" @cancel="close">
@ok="submit" @cancel="close">
<a-form :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }"> <a-form :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
<a-form-item :label="t('pages.clients.attachedInbounds') || 'Attached inbounds'" required> <a-form-item :label="t('pages.clients.attachedInbounds') || 'Attached inbounds'" required>
<a-select v-model:value="form.inboundIds" mode="multiple" :options="inboundOptions" <a-select v-model:value="form.inboundIds" mode="multiple" :options="inboundOptions"
@ -231,11 +226,11 @@ async function submit() {
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item :label="t('pages.clients.limitIp') || 'IP Limit'"> <a-form-item v-if="ipLimitEnable" :label="t('pages.clients.limitIp') || 'IP Limit'">
<a-input-number v-model:value="form.limitIp" :min="0" :disabled="!ipLimitEnable" /> <a-input-number v-model:value="form.limitIp" :min="0" />
</a-form-item> </a-form-item>
<a-form-item :label="t('pages.clients.totalGB') || 'Total (GB)'"> <a-form-item :label="t('pages.clients.totalGB')">
<a-input-number v-model:value="form.totalGB" :min="0" :step="0.1" /> <a-input-number v-model:value="form.totalGB" :min="0" :step="0.1" />
</a-form-item> </a-form-item>

View file

@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { HttpUtil, RandomUtil } from '@/utils'; import { HttpUtil, RandomUtil } from '@/utils';
import { DBInbound } from '@/models/dbinbound.js';
import { TLS_FLOW_CONTROL } from '@/models/inbound.js'; import { TLS_FLOW_CONTROL } from '@/models/inbound.js';
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL); const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
@ -37,6 +36,7 @@ function emptyForm() {
totalGB: 0, totalGB: 0,
expiryTime: null, expiryTime: null,
limitIp: 0, limitIp: 0,
tgId: 0,
comment: '', comment: '',
enable: true, enable: true,
inboundIds: [], inboundIds: [],
@ -61,6 +61,7 @@ watch(
form.totalGB = bytesToGB(props.client.totalGB || 0); form.totalGB = bytesToGB(props.client.totalGB || 0);
form.expiryTime = props.client.expiryTime ? dayjs(props.client.expiryTime) : null; form.expiryTime = props.client.expiryTime ? dayjs(props.client.expiryTime) : null;
form.limitIp = props.client.limitIp || 0; form.limitIp = props.client.limitIp || 0;
form.tgId = Number(props.client.tgId) || 0;
form.comment = props.client.comment || ''; form.comment = props.client.comment || '';
form.enable = !!props.client.enable; form.enable = !!props.client.enable;
form.inboundIds = Array.isArray(props.attachedIds) ? [...props.attachedIds] : []; form.inboundIds = Array.isArray(props.attachedIds) ? [...props.attachedIds] : [];
@ -102,10 +103,7 @@ const inboundOptions = computed(() =>
const flowCapableIds = computed(() => { const flowCapableIds = computed(() => {
const ids = new Set(); const ids = new Set();
for (const row of props.inbounds || []) { for (const row of props.inbounds || []) {
try { if (row?.tlsFlowCapable) ids.add(row.id);
const parsed = new DBInbound(row).toInbound();
if (parsed.canEnableTlsFlow?.()) ids.add(row.id);
} catch (_e) { /* ignore unparsable */ }
} }
return ids; return ids;
}); });
@ -185,7 +183,7 @@ function regenerateSubId() {
} }
function regenerateEmail() { function regenerateEmail() {
form.email = RandomUtil.randomLowerAndNum(9); form.email = RandomUtil.randomLowerAndNum(12);
} }
async function onSubmit() { async function onSubmit() {
@ -207,6 +205,7 @@ async function onSubmit() {
totalGB: gbToBytes(form.totalGB), totalGB: gbToBytes(form.totalGB),
expiryTime: form.expiryTime ? form.expiryTime.valueOf() : 0, expiryTime: form.expiryTime ? form.expiryTime.valueOf() : 0,
limitIp: Number(form.limitIp) || 0, limitIp: Number(form.limitIp) || 0,
tgId: Number(form.tgId) || 0,
comment: form.comment, comment: form.comment,
enable: !!form.enable, enable: !!form.enable,
}; };
@ -268,10 +267,10 @@ async function onSubmit() {
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="12"> <a-col :span="12">
<a-form-item label="UUID"> <a-form-item label="Auth (Hysteria)">
<a-input-group compact style="display: flex"> <a-input-group compact style="display: flex">
<a-input v-model:value="form.uuid" style="flex: 1" /> <a-input v-model:value="form.auth" style="flex: 1" />
<a-button @click="regenerateUUID"></a-button> <a-button @click="regenerateAuth"></a-button>
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -286,17 +285,17 @@ async function onSubmit() {
</a-row> </a-row>
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="12"> <a-col :span="ipLimitEnable ? 12 : 24">
<a-form-item label="Auth (Hysteria)"> <a-form-item label="UUID">
<a-input-group compact style="display: flex"> <a-input-group compact style="display: flex">
<a-input v-model:value="form.auth" style="flex: 1" /> <a-input v-model:value="form.uuid" style="flex: 1" />
<a-button @click="regenerateAuth"></a-button> <a-button @click="regenerateUUID"></a-button>
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col v-if="ipLimitEnable" :span="12">
<a-form-item :label="t('pages.clients.limitIp') || 'IP limit'"> <a-form-item :label="t('pages.clients.limitIp') || 'IP limit'">
<a-input-number v-model:value="form.limitIp" :min="0" :disabled="!ipLimitEnable" style="width: 100%" /> <a-input-number v-model:value="form.limitIp" :min="0" style="width: 100%" />
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
@ -330,9 +329,20 @@ async function onSubmit() {
</a-col> </a-col>
</a-row> </a-row>
<a-form-item :label="t('pages.clients.comment') || 'Comment'"> <a-row :gutter="16">
<a-input v-model:value="form.comment" /> <a-col :span="12">
</a-form-item> <a-form-item :label="'Telegram user ID'">
<a-input-number v-model:value="form.tgId" :min="0" :controls="false"
:placeholder="t('pages.clients.telegramIdPlaceholder') || 'Numeric Telegram user ID (0 = none)'"
style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="t('pages.clients.comment') || 'Comment'">
<a-input v-model:value="form.comment" />
</a-form-item>
</a-col>
</a-row>
<a-form-item :label="t('pages.clients.attachedInbounds') || 'Attached inbounds'" :required="!isEdit"> <a-form-item :label="t('pages.clients.attachedInbounds') || 'Attached inbounds'" :required="!isEdit">
<a-select v-model:value="form.inboundIds" mode="multiple" :options="inboundOptions" :show-search="true" <a-select v-model:value="form.inboundIds" mode="multiple" :options="inboundOptions" :show-search="true"

View file

@ -19,7 +19,7 @@ export function useClients() {
try { try {
const [clientsMsg, inboundsMsg] = await Promise.all([ const [clientsMsg, inboundsMsg] = await Promise.all([
HttpUtil.get('/panel/api/clients/list'), HttpUtil.get('/panel/api/clients/list'),
HttpUtil.get('/panel/api/inbounds/list'), HttpUtil.get('/panel/api/inbounds/options'),
]); ]);
if (clientsMsg?.success) { if (clientsMsg?.success) {
clients.value = Array.isArray(clientsMsg.obj) ? clientsMsg.obj : []; clients.value = Array.isArray(clientsMsg.obj) ? clientsMsg.obj : [];

View file

@ -61,6 +61,7 @@ func (a *InboundController) broadcastInboundsUpdate(userId int) {
func (a *InboundController) initRouter(g *gin.RouterGroup) { func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.GET("/list", a.getInbounds) g.GET("/list", a.getInbounds)
g.GET("/options", a.getInboundOptions)
g.GET("/get/:id", a.getInbound) g.GET("/get/:id", a.getInbound)
g.GET("/:id/fallbackChildren", a.getFallbackChildren) g.GET("/:id/fallbackChildren", a.getFallbackChildren)
@ -85,6 +86,19 @@ func (a *InboundController) getInbounds(c *gin.Context) {
jsonObj(c, inbounds, nil) jsonObj(c, inbounds, nil)
} }
// getInboundOptions returns a lightweight projection of the user's inbounds
// (id, remark, protocol, port, tlsFlowCapable) for pickers in the clients UI.
// Avoids shipping per-client settings and traffic stats just to fill a dropdown.
func (a *InboundController) getInboundOptions(c *gin.Context) {
user := session.GetLoginUser(c)
options, err := a.inboundService.GetInboundOptions(user.Id)
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
return
}
jsonObj(c, options, nil)
}
// getInbound retrieves a specific inbound by its ID. // getInbound retrieves a specific inbound by its ID.
func (a *InboundController) getInbound(c *gin.Context) { func (a *InboundController) getInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))

View file

@ -134,6 +134,75 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
return inbounds, nil return inbounds, nil
} }
// InboundOption is the lightweight projection of an inbound used by client UI
// pickers — only the fields needed to render labels, filter by protocol, and
// decide whether the XTLS Vision flow selector should appear. Keeping this
// payload minimal avoids shipping per-client settings and traffic stats just
// to populate a dropdown.
type InboundOption struct {
Id int `json:"id"`
Remark string `json:"remark"`
Protocol string `json:"protocol"`
Port int `json:"port"`
TlsFlowCapable bool `json:"tlsFlowCapable"`
}
// GetInboundOptions returns the picker-sized projection of the user's inbounds.
// The TlsFlowCapable flag mirrors Inbound.canEnableTlsFlow() on the frontend
// (VLESS/PortFallback over TCP with tls or reality) so the client modal does
// not need StreamSettings to decide whether to show the Flow field.
func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error) {
db := database.GetDB()
var rows []struct {
Id int `gorm:"column:id"`
Remark string `gorm:"column:remark"`
Protocol string `gorm:"column:protocol"`
Port int `gorm:"column:port"`
StreamSettings string `gorm:"column:stream_settings"`
}
err := db.Table("inbounds").
Select("id, remark, protocol, port, stream_settings").
Where("user_id = ?", userId).
Order("id ASC").
Scan(&rows).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
out := make([]InboundOption, 0, len(rows))
for _, r := range rows {
out = append(out, InboundOption{
Id: r.Id,
Remark: r.Remark,
Protocol: r.Protocol,
Port: r.Port,
TlsFlowCapable: inboundCanEnableTlsFlow(r.Protocol, r.StreamSettings),
})
}
return out, nil
}
// inboundCanEnableTlsFlow mirrors Inbound.canEnableTlsFlow() from the frontend:
// XTLS Vision is only valid for VLESS / PortFallback on TCP with tls or reality.
func inboundCanEnableTlsFlow(protocol, streamSettings string) bool {
if protocol != string(model.VLESS) && protocol != string(model.PortFallback) {
return false
}
if streamSettings == "" {
return false
}
var stream struct {
Network string `json:"network"`
Security string `json:"security"`
}
if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil {
return false
}
if stream.Network != "tcp" {
return false
}
return stream.Security == "tls" || stream.Security == "reality"
}
// GetAllInbounds retrieves all inbounds with client stats. // GetAllInbounds retrieves all inbounds with client stats.
func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) { func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()

View file

@ -31,7 +31,6 @@ import (
"github.com/mhsanaei/3x-ui/v3/web/locale" "github.com/mhsanaei/3x-ui/v3/web/locale"
"github.com/mhsanaei/3x-ui/v3/xray" "github.com/mhsanaei/3x-ui/v3/xray"
"github.com/google/uuid"
"github.com/mymmrac/telego" "github.com/mymmrac/telego"
th "github.com/mymmrac/telego/telegohandler" th "github.com/mymmrac/telego/telegohandler"
tu "github.com/mymmrac/telego/telegoutil" tu "github.com/mymmrac/telego/telegoutil"
@ -73,23 +72,23 @@ var (
mutex sync.RWMutex mutex sync.RWMutex
} }
// clients data to adding new client // clients data to adding new client. receiver_inbound_IDs is the set of
receiver_inbound_ID int // inbounds the new client will be attached to; receiver_inbound_ID mirrors
client_Id string // the primary pick for the legacy attach-picker entry point. Per-protocol
client_Flow string // secrets (UUID, password, flow, method) are filled per-inbound on submit
client_Email string // by ClientService.fillProtocolDefaults, so the bot only tracks universal
client_LimitIP int // client fields here.
client_TotalGB int64 receiver_inbound_ID int
client_ExpiryTime int64 receiver_inbound_IDs []int
client_Enable bool client_Email string
client_TgID string client_LimitIP int
client_SubID string client_TotalGB int64
client_Comment string client_ExpiryTime int64
client_Reset int client_Enable bool
client_Security string client_TgID string
client_ShPassword string client_SubID string
client_TrPassword string client_Comment string
client_Method string client_Reset int
) )
var userStates = make(map[int64]string) var userStates = make(map[int64]string)
@ -506,84 +505,6 @@ func (t *Tgbot) OnReceive() {
h.HandleMessage(func(ctx *th.Context, message telego.Message) error { h.HandleMessage(func(ctx *th.Context, message telego.Message) error {
if userState, exists := userStates[message.Chat.ID]; exists { if userState, exists := userStates[message.Chat.ID]; exists {
switch userState { switch userState {
case "awaiting_id":
if client_Id == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
return nil
}
client_Id = strings.TrimSpace(message.Text)
if t.isSingleWord(client_Id) {
userStates[message.Chat.ID] = "awaiting_id"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_id"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_password_tr":
if client_TrPassword == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_TrPassword = strings.TrimSpace(message.Text)
if t.isSingleWord(client_TrPassword) {
userStates[message.Chat.ID] = "awaiting_password_tr"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_password_sh":
if client_ShPassword == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_ShPassword = strings.TrimSpace(message.Text)
if t.isSingleWord(client_ShPassword) {
userStates[message.Chat.ID] = "awaiting_password_sh"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_email": case "awaiting_email":
if client_Email == strings.TrimSpace(message.Text) { if client_Email == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
@ -605,9 +526,7 @@ func (t *Tgbot) OnReceive() {
} else { } else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_email"), 3, tu.ReplyKeyboardRemove()) t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_email"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) t.addClient(message.Chat.ID, t.BuildClientDraftMessage())
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
} }
case "awaiting_comment": case "awaiting_comment":
if client_Comment == strings.TrimSpace(message.Text) { if client_Comment == strings.TrimSpace(message.Text) {
@ -619,9 +538,29 @@ func (t *Tgbot) OnReceive() {
client_Comment = strings.TrimSpace(message.Text) client_Comment = strings.TrimSpace(message.Text)
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_comment"), 3, tu.ReplyKeyboardRemove()) t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_comment"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) t.addClient(message.Chat.ID, t.BuildClientDraftMessage())
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) case "awaiting_tg_id":
t.addClient(message.Chat.ID, message_text) input := strings.TrimSpace(message.Text)
if input == "" || input == "-" || strings.EqualFold(input, "none") {
client_TgID = ""
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
t.addClient(message.Chat.ID, t.BuildClientDraftMessage())
return nil
}
if _, err := strconv.ParseInt(input, 10, 64); err != nil {
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
return nil
}
client_TgID = input
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.userSaved"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
t.addClient(message.Chat.ID, t.BuildClientDraftMessage())
} }
} else { } else {
@ -749,16 +688,6 @@ func (t *Tgbot) randomLowerAndNum(length int) string {
return string(bytes) return string(bytes)
} }
// randomShadowSocksPassword generates a random password for Shadowsocks.
func (t *Tgbot) randomShadowSocksPassword() string {
array := make([]byte, 32)
_, err := rand.Read(array)
if err != nil {
return t.randomLowerAndNum(32)
}
return base64.StdEncoding.EncodeToString(array)
}
// answerCallback processes callback queries from inline keyboards. // answerCallback processes callback queries from inline keyboards.
func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) { func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {
chatId := callbackQuery.Message.GetChat().ID chatId := callbackQuery.Message.GetChat().ID
@ -979,16 +908,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
limitTraffic, _ := strconv.ParseInt(dataArray[1], 10, 64) limitTraffic, _ := strconv.ParseInt(dataArray[1], 10, 64)
client_TotalGB = limitTraffic * 1024 * 1024 * 1024 client_TotalGB = limitTraffic * 1024 * 1024 * 1024
messageId := callbackQuery.Message.GetMessageID() messageId := callbackQuery.Message.GetMessageID()
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) message_text := t.BuildClientDraftMessage()
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.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
@ -1200,16 +1120,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
client_ExpiryTime = date client_ExpiryTime = date
messageId := callbackQuery.Message.GetMessageID() messageId := callbackQuery.Message.GetMessageID()
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) message_text := t.BuildClientDraftMessage()
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.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
@ -1388,36 +1299,8 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
} }
messageId := callbackQuery.Message.GetMessageID() messageId := callbackQuery.Message.GetMessageID()
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) message_text := t.BuildClientDraftMessage()
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_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.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
case "add_client_ip_limit_in": case "add_client_ip_limit_in":
@ -1574,9 +1457,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
} }
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseClient", "Inbound=="+inbound.Remark), clients) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseClient", "Inbound=="+inbound.Remark), clients)
case "add_client_to": case "add_client_to":
// assign default values to clients variables
client_Id = uuid.New().String()
client_Flow = ""
client_Email = t.randomLowerAndNum(8) client_Email = t.randomLowerAndNum(8)
client_LimitIP = 0 client_LimitIP = 0
client_TotalGB = 0 client_TotalGB = 0
@ -1586,10 +1466,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
client_SubID = t.randomLowerAndNum(16) client_SubID = t.randomLowerAndNum(16)
client_Comment = "" client_Comment = ""
client_Reset = 0 client_Reset = 0
client_Security = "auto"
client_ShPassword = t.randomShadowSocksPassword()
client_TrPassword = t.randomLowerAndNum(10)
client_Method = ""
inboundId := dataArray[1] inboundId := dataArray[1]
inboundIdInt, err := strconv.Atoi(inboundId) inboundIdInt, err := strconv.Atoi(inboundId)
@ -1598,19 +1474,33 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return return
} }
receiver_inbound_ID = inboundIdInt receiver_inbound_ID = inboundIdInt
inbound, err := t.inboundService.GetInbound(inboundIdInt) receiver_inbound_IDs = []int{inboundIdInt}
t.addClient(callbackQuery.Message.GetChat().ID, t.BuildClientDraftMessage())
case "add_client_toggle_attach":
inboundIdStr := dataArray[1]
inboundIdInt, err := strconv.Atoi(inboundIdStr)
if err != nil { if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return return
} }
found := -1
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) for i, id := range receiver_inbound_IDs {
if id == inboundIdInt {
found = i
break
}
}
if found >= 0 {
receiver_inbound_IDs = append(receiver_inbound_IDs[:found], receiver_inbound_IDs[found+1:]...)
} else {
receiver_inbound_IDs = append(receiver_inbound_IDs, inboundIdInt)
}
picker, err := t.getInboundsAttachPicker()
if err != nil { if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return return
} }
t.editMessageCallbackTgBot(callbackQuery.Message.GetChat().ID, callbackQuery.Message.GetMessageID(), picker)
t.addClient(callbackQuery.Message.GetChat().ID, message_text)
} }
return return
} else { } else {
@ -1753,9 +1643,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands"))
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpAdminCommands")) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpAdminCommands"))
case "add_client": case "add_client":
// assign default values to clients variables
client_Id = uuid.New().String()
client_Flow = ""
client_Email = t.randomLowerAndNum(8) client_Email = t.randomLowerAndNum(8)
client_LimitIP = 0 client_LimitIP = 0
client_TotalGB = 0 client_TotalGB = 0
@ -1765,10 +1652,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
client_SubID = t.randomLowerAndNum(16) client_SubID = t.randomLowerAndNum(16)
client_Comment = "" client_Comment = ""
client_Reset = 0 client_Reset = 0
client_Security = "auto"
client_ShPassword = t.randomShadowSocksPassword()
client_TrPassword = t.randomLowerAndNum(10)
client_Method = ""
inbounds, err := t.getInboundsAddClient() inbounds, err := t.getInboundsAddClient()
if err != nil { if err != nil {
@ -1787,36 +1670,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
) )
prompt_message := t.I18nBot("tgbot.messages.email_prompt", "ClientEmail=="+client_Email) prompt_message := t.I18nBot("tgbot.messages.email_prompt", "ClientEmail=="+client_Email)
t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup) t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)
case "add_client_ch_default_id":
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
userStates[chatId] = "awaiting_id"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
prompt_message := t.I18nBot("tgbot.messages.id_prompt", "ClientId=="+client_Id)
t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)
case "add_client_ch_default_pass_tr":
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
userStates[chatId] = "awaiting_password_tr"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
prompt_message := t.I18nBot("tgbot.messages.pass_prompt", "ClientPassword=="+client_TrPassword)
t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)
case "add_client_ch_default_pass_sh":
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
userStates[chatId] = "awaiting_password_sh"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
prompt_message := t.I18nBot("tgbot.messages.pass_prompt", "ClientPassword=="+client_ShPassword)
t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)
case "add_client_ch_default_comment": case "add_client_ch_default_comment":
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
userStates[chatId] = "awaiting_comment" userStates[chatId] = "awaiting_comment"
@ -1827,6 +1680,19 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
) )
prompt_message := t.I18nBot("tgbot.messages.comment_prompt", "ClientComment=="+client_Comment) prompt_message := t.I18nBot("tgbot.messages.comment_prompt", "ClientComment=="+client_Comment)
t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup) t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)
case "add_client_ch_default_tg_id":
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
userStates[chatId] = "awaiting_tg_id"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
current := client_TgID
if current == "" {
current = "—"
}
t.SendMsgToTgbot(chatId, fmt.Sprintf("Send the Telegram user id (numeric) to attach to this client, or send `-` / `none` to clear.\nCurrent: `%s`", current), cancel_btn_markup)
case "add_client_ch_default_traffic": case "add_client_ch_default_traffic":
inlineKeyboard := tu.InlineKeyboard( inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
@ -1885,22 +1751,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
), ),
) )
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) 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": case "add_client_ch_default_ip_limit":
inlineKeyboard := tu.InlineKeyboard( inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
@ -1934,41 +1784,41 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
t.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) t.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, chatId) delete(userStates, chatId)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) t.addClient(chatId, t.BuildClientDraftMessage())
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(chatId, message_text)
case "add_client_cancel": case "add_client_cancel":
delete(userStates, chatId) delete(userStates, chatId)
receiver_inbound_ID = 0
receiver_inbound_IDs = nil
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
t.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot("tgbot.messages.cancel"), 3, tu.ReplyKeyboardRemove()) t.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot("tgbot.messages.cancel"), 3, tu.ReplyKeyboardRemove())
case "add_client_default_traffic_exp": case "add_client_default_traffic_exp":
messageId := callbackQuery.Message.GetMessageID() messageId := callbackQuery.Message.GetMessageID()
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) message_text := t.BuildClientDraftMessage()
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(chatId, message_text, messageId) t.addClient(chatId, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email))
case "add_client_default_ip_limit": case "add_client_default_ip_limit":
messageId := callbackQuery.Message.GetMessageID() messageId := callbackQuery.Message.GetMessageID()
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) message_text := t.BuildClientDraftMessage()
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(chatId, message_text, messageId) t.addClient(chatId, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email))
case "add_client_attach_more":
picker, err := t.getInboundsAttachPicker()
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.SendMsgToTgbot(chatId, "Pick inbound(s) to attach:", picker)
case "add_client_attach_done":
if receiver_inbound_ID == 0 && len(receiver_inbound_IDs) > 0 {
receiver_inbound_ID = receiver_inbound_IDs[0]
}
if receiver_inbound_ID == 0 {
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.getInboundsFailed"))
return
}
message_text := t.BuildClientDraftMessage()
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
t.addClient(chatId, message_text)
case "add_client_submit_disable": case "add_client_submit_disable":
client_Enable = false client_Enable = false
_, err := t.SubmitAddClient() _, err := t.SubmitAddClient()
@ -1980,6 +1830,8 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove()) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove())
t.sendClientIndividualLinks(chatId, client_Email) t.sendClientIndividualLinks(chatId, client_Email)
t.sendClientQRLinks(chatId, client_Email) t.sendClientQRLinks(chatId, client_Email)
receiver_inbound_ID = 0
receiver_inbound_IDs = nil
} }
case "add_client_submit_enable": case "add_client_submit_enable":
client_Enable = true client_Enable = true
@ -1992,6 +1844,8 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove()) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove())
t.sendClientIndividualLinks(chatId, client_Email) t.sendClientIndividualLinks(chatId, client_Email)
t.sendClientQRLinks(chatId, client_Email) t.sendClientQRLinks(chatId, client_Email)
receiver_inbound_ID = 0
receiver_inbound_IDs = nil
} }
case "reset_all_traffics_cancel": case "reset_all_traffics_cancel":
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
@ -2081,140 +1935,98 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
} }
} }
// BuildInboundClientDataMessage builds a message with client data for the given inbound and protocol. // BuildClientDraftMessage builds a protocol-neutral summary of the in-progress
func (t *Tgbot) BuildInboundClientDataMessage(inbound_remark string, protocol model.Protocol) (string, error) { // client (email, attached inbounds, traffic limit, expiry, ip limit, comment)
var message string // shown in the multi-inbound add flow. Per-protocol secrets (UUID, password,
// flow, method) are generated by fillProtocolDefaults on submit, so the bot
currentTime := time.Now() // never has to track them per inbound itself.
timestampMillis := currentTime.UnixNano() / int64(time.Millisecond) func (t *Tgbot) BuildClientDraftMessage() string {
now := time.Now().UnixMilli()
expiryTime := ""
diff := client_ExpiryTime/1000 - timestampMillis
if client_ExpiryTime == 0 {
expiryTime = t.I18nBot("tgbot.unlimited")
} else if diff > 172800 {
expiryTime = time.Unix((client_ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
} else if client_ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d %s", client_ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
} else {
expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
}
traffic_value := ""
if client_TotalGB == 0 {
traffic_value = "♾️ Unlimited(Reset)"
} else {
traffic_value = common.FormatTraffic(client_TotalGB)
}
ip_limit := ""
if client_LimitIP == 0 {
ip_limit = "♾️ Unlimited(Reset)"
} else {
ip_limit = fmt.Sprint(client_LimitIP)
}
switch protocol {
case model.VMESS, model.VLESS:
message = t.I18nBot("tgbot.messages.inbound_client_data_id", "InboundRemark=="+inbound_remark, "ClientId=="+client_Id, "ClientEmail=="+client_Email, "ClientTraffic=="+traffic_value, "ClientExp=="+expiryTime, "IpLimit=="+ip_limit, "ClientComment=="+client_Comment)
case model.Trojan:
message = t.I18nBot("tgbot.messages.inbound_client_data_pass", "InboundRemark=="+inbound_remark, "ClientPass=="+client_TrPassword, "ClientEmail=="+client_Email, "ClientTraffic=="+traffic_value, "ClientExp=="+expiryTime, "IpLimit=="+ip_limit, "ClientComment=="+client_Comment)
case model.Shadowsocks:
message = t.I18nBot("tgbot.messages.inbound_client_data_pass", "InboundRemark=="+inbound_remark, "ClientPass=="+client_ShPassword, "ClientEmail=="+client_Email, "ClientTraffic=="+traffic_value, "ClientExp=="+expiryTime, "IpLimit=="+ip_limit, "ClientComment=="+client_Comment)
expiry := ""
switch {
case client_ExpiryTime == 0:
expiry = t.I18nBot("tgbot.unlimited")
case client_ExpiryTime < 0:
expiry = fmt.Sprintf("%d %s", client_ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
default: default:
return "", errors.New("unknown protocol") diff := client_ExpiryTime - now
if diff > 172800000 {
expiry = time.UnixMilli(client_ExpiryTime).Format("2006-01-02 15:04:05")
} else {
expiry = fmt.Sprintf("%d %s", diff/3600000, t.I18nBot("tgbot.hours"))
}
} }
return message, nil traffic := "♾️ Unlimited(Reset)"
if client_TotalGB > 0 {
traffic = common.FormatTraffic(client_TotalGB)
}
ipLimit := "♾️ Unlimited(Reset)"
if client_LimitIP > 0 {
ipLimit = fmt.Sprint(client_LimitIP)
}
attached := t.describeAttachedInbounds(receiver_inbound_IDs)
if attached == "" {
attached = "—"
}
comment := client_Comment
if comment == "" {
comment = "—"
}
tgID := client_TgID
if tgID == "" {
tgID = "—"
}
var b strings.Builder
b.WriteString("📝 *New client draft*\r\n")
b.WriteString(fmt.Sprintf("📧 Email: `%s`\r\n", client_Email))
b.WriteString(fmt.Sprintf("🔗 Attached: %s\r\n", attached))
b.WriteString(fmt.Sprintf("📊 Traffic: %s\r\n", traffic))
b.WriteString(fmt.Sprintf("📅 Expire: %s\r\n", expiry))
b.WriteString(fmt.Sprintf("🔢 IP limit: %s\r\n", ipLimit))
b.WriteString(fmt.Sprintf("👤 TG user: %s\r\n", tgID))
b.WriteString(fmt.Sprintf("💬 Comment: %s\r\n", comment))
return b.String()
} }
// BuildJSONForProtocol builds a JSON string for the given protocol with client data. // describeAttachedInbounds returns a short "remark1, remark2" list for the given
func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) { // inbound ids, falling back to "#id" when an inbound can't be loaded.
var jsonString string func (t *Tgbot) describeAttachedInbounds(ids []int) string {
if len(ids) == 0 {
switch protocol { return ""
case model.VMESS:
jsonString = fmt.Sprintf(`{
"clients": [{
"id": "%s",
"security": "%s",
"email": "%s",
"limitIp": %d,
"totalGB": %d,
"expiryTime": %d,
"enable": %t,
"tgId": "%s",
"subId": "%s",
"comment": "%s",
"reset": %d
}]
}`, client_Id, client_Security, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset)
case model.VLESS:
jsonString = fmt.Sprintf(`{
"clients": [{
"id": "%s",
"flow": "%s",
"email": "%s",
"limitIp": %d,
"totalGB": %d,
"expiryTime": %d,
"enable": %t,
"tgId": "%s",
"subId": "%s",
"comment": "%s",
"reset": %d
}]
}`, client_Id, client_Flow, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset)
case model.Trojan:
jsonString = fmt.Sprintf(`{
"clients": [{
"password": "%s",
"email": "%s",
"limitIp": %d,
"totalGB": %d,
"expiryTime": %d,
"enable": %t,
"tgId": "%s",
"subId": "%s",
"comment": "%s",
"reset": %d
}]
}`, client_TrPassword, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset)
case model.Shadowsocks:
jsonString = fmt.Sprintf(`{
"clients": [{
"method": "%s",
"password": "%s",
"email": "%s",
"limitIp": %d,
"totalGB": %d,
"expiryTime": %d,
"enable": %t,
"tgId": "%s",
"subId": "%s",
"comment": "%s",
"reset": %d
}]
}`, client_Method, client_ShPassword, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset)
default:
return "", errors.New("unknown protocol")
} }
parts := make([]string, 0, len(ids))
return jsonString, nil for _, id := range ids {
ib, err := t.inboundService.GetInbound(id)
if err != nil || ib == nil {
parts = append(parts, fmt.Sprintf("#%d", id))
continue
}
label := ib.Remark
if label == "" {
label = fmt.Sprintf("#%d", id)
}
parts = append(parts, label)
}
return strings.Join(parts, ", ")
} }
// SubmitAddClient submits the client addition request to the client service. // SubmitAddClient sends the in-progress client to ClientService.Create with
// the full set of attached inbound ids. Per-inbound fillProtocolDefaults on
// the panel generates UUID/password/auth per protocol, so the bot only
// supplies the universal fields it actually collected.
func (t *Tgbot) SubmitAddClient() (bool, error) { func (t *Tgbot) SubmitAddClient() (bool, error) {
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) inboundIDs := receiver_inbound_IDs
if err != nil { if len(inboundIDs) == 0 && receiver_inbound_ID > 0 {
logger.Warning("getIboundClients run failed:", err) inboundIDs = []int{receiver_inbound_ID}
}
if len(inboundIDs) == 0 {
return false, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) return false, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
} }
@ -2231,22 +2043,10 @@ func (t *Tgbot) SubmitAddClient() (bool, error) {
TgID: tgIDInt, TgID: tgIDInt,
} }
switch inbound.Protocol { return t.clientService.Create(&t.inboundService, &ClientCreatePayload{
case model.VMESS: Client: client,
client.ID = client_Id InboundIds: inboundIDs,
client.Security = client_Security })
case model.VLESS:
client.ID = client_Id
client.Flow = client_Flow
case model.Trojan:
client.Password = client_TrPassword
case model.Shadowsocks:
client.Password = client_ShPassword
default:
return false, errors.New("unknown protocol")
}
return t.clientService.CreateOne(&t.inboundService, receiver_inbound_ID, client)
} }
// checkAdmin checks if the given Telegram ID is an admin. // checkAdmin checks if the given Telegram ID is an admin.
@ -2864,26 +2664,28 @@ func (t *Tgbot) UserLoginNotify(attempt LoginAttempt) {
// getInboundUsages retrieves and formats inbound usage information. // getInboundUsages retrieves and formats inbound usage information.
func (t *Tgbot) getInboundUsages() string { func (t *Tgbot) getInboundUsages() string {
var info strings.Builder var info strings.Builder
// get traffic
inbounds, err := t.inboundService.GetAllInbounds() inbounds, err := t.inboundService.GetAllInbounds()
if err != nil { if err != nil {
logger.Warning("GetAllInbounds run failed:", err) logger.Warning("GetAllInbounds run failed:", err)
info.WriteString(t.I18nBot("tgbot.answers.getInboundsFailed")) info.WriteString(t.I18nBot("tgbot.answers.getInboundsFailed"))
} else { return info.String()
// NOTE:If there no any sessions here,need to notify here }
// TODO:Sub-node push, automatic conversion format for _, inbound := range inbounds {
for _, inbound := range inbounds { info.WriteString(t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark))
info.WriteString(t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)) info.WriteString(t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)))
info.WriteString(t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port))) info.WriteString(t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)))
info.WriteString(t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)))
if inbound.ExpiryTime == 0 { clients, listErr := t.clientService.ListForInbound(nil, inbound.Id)
info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited"))) if listErr == nil {
} else { info.WriteString(fmt.Sprintf("👥 Clients: %d\r\n", len(clients)))
info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")))
}
info.WriteString("\r\n")
} }
if inbound.ExpiryTime == 0 {
info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited")))
} else {
info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")))
}
info.WriteString("\r\n")
} }
return info.String() return info.String()
} }
@ -3030,6 +2832,54 @@ func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
return keyboard, nil return keyboard, nil
} }
// getInboundsAttachPicker builds a toggle picker over multi-client inbounds
// for the "attach more inbounds to the new client" step. Each row shows the
// current selection state for the inbound; tapping fires
// add_client_toggle_attach <id> which flips it and re-renders. A final
// "Done" button (add_client_attach_done) returns to the field-edit screen.
func (t *Tgbot) getInboundsAttachPicker() (*telego.InlineKeyboardMarkup, error) {
inbounds, err := t.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("GetAllInbounds run failed:", err)
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
}
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.HTTP: true,
}
selected := make(map[int]bool, len(receiver_inbound_IDs))
for _, id := range receiver_inbound_IDs {
selected[id] = true
}
var buttons []telego.InlineKeyboardButton
for _, ib := range inbounds {
if excludedProtocols[ib.Protocol] {
continue
}
mark := "☐"
if selected[ib.Id] {
mark = "✅"
}
label := fmt.Sprintf("%s %s (%s)", mark, ib.Remark, ib.Protocol)
callback := t.encodeQuery(fmt.Sprintf("add_client_toggle_attach %d", ib.Id))
buttons = append(buttons, tu.InlineKeyboardButton(label).WithCallbackData(callback))
}
cols := 1
if len(buttons) >= 6 {
cols = 2
}
rows := tu.InlineKeyboardCols(cols, buttons...)
rows = append(rows, tu.InlineKeyboardRow(
tu.InlineKeyboardButton("✅ Done").WithCallbackData(t.encodeQuery("add_client_attach_done")),
))
return tu.InlineKeyboardGrid(rows), nil
}
// getInboundClients creates an inline keyboard with clients of a specific inbound. // getInboundClients creates an inline keyboard with clients of a specific inbound.
func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error) { func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error) {
inbound, err := t.inboundService.GetInbound(id) inbound, err := t.inboundService.GetInbound(id)
@ -3143,6 +2993,9 @@ func (t *Tgbot) clientInfoMsg(
output := "" output := ""
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
if attachIds, err := t.clientService.GetInboundIdsForEmail(nil, traffic.Email); err == nil && len(attachIds) > 0 {
output += fmt.Sprintf("🔗 Inbounds: %s\r\n", t.describeAttachedInbounds(attachIds))
}
if printEnabled { if printEnabled {
output += t.I18nBot("tgbot.messages.enabled", "Enable=="+enabled) output += t.I18nBot("tgbot.messages.enabled", "Enable=="+enabled)
} }
@ -3376,16 +3229,27 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
} }
} }
// getCommonClientButtons returns the shared inline keyboard rows for client configuration // getCommonClientButtons returns the shared inline keyboard rows for the
// client-first multi-inbound add flow. Per-protocol secrets (UUID, password,
// flow, method) are generated by fillProtocolDefaults on submit, so the bot
// only exposes the universal client fields here.
func (t *Tgbot) getCommonClientButtons() [][]telego.InlineKeyboardButton { func (t *Tgbot) getCommonClientButtons() [][]telego.InlineKeyboardButton {
attachLabel := fmt.Sprintf(" Attach inbound (%d)", len(receiver_inbound_IDs))
return [][]telego.InlineKeyboardButton{ return [][]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_comment")).WithCallbackData("add_client_ch_default_comment"),
),
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"),
), ),
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData("add_client_ch_default_ip_limit"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData("add_client_ch_default_ip_limit"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.setTGUser")).WithCallbackData("add_client_ch_default_tg_id"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(attachLabel).WithCallbackData("add_client_attach_more"),
), ),
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"),
@ -3397,87 +3261,14 @@ func (t *Tgbot) getCommonClientButtons() [][]telego.InlineKeyboardButton {
} }
} }
// inboundCanEnableTlsFlow mirrors Inbound.canEnableTlsFlow() from the frontend // addClient renders the draft message + shared client-first keyboard.
// 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) { func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) inlineKeyboard := tu.InlineKeyboard(t.getCommonClientButtons()...)
if err != nil {
t.SendMsgToTgbot(chatId, err.Error())
return
}
protocol := inbound.Protocol
var protocolRows [][]telego.InlineKeyboardButton
switch protocol {
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(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_tr"),
),
}
case model.Shadowsocks:
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_password")).WithCallbackData("add_client_ch_default_pass_sh"),
),
}
}
commonRows := t.getCommonClientButtons()
inlineKeyboard := tu.InlineKeyboard(append(protocolRows, commonRows...)...)
if len(messageID) > 0 { if len(messageID) > 0 {
t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard) t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard)
} else { } else {
t.SendMsgToTgbot(chatId, msg, inlineKeyboard) t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
} }
} }
// searchInbound searches for inbounds by remark and sends the results. // searchInbound searches for inbounds by remark and sends the results.