mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
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:
parent
14ad255c38
commit
d10fa8f3c0
7 changed files with 401 additions and 515 deletions
|
|
@ -80,6 +80,13 @@ export const sections = [
|
|||
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}',
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/api/inbounds/options',
|
||||
summary: 'Lightweight picker projection of the authenticated user’s 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',
|
||||
path: '/panel/api/inbounds/get/:id',
|
||||
|
|
@ -392,22 +399,22 @@ export const sections = [
|
|||
{
|
||||
method: 'POST',
|
||||
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: [
|
||||
{ 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.' },
|
||||
],
|
||||
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}',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
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: [
|
||||
{ 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}',
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { message } from 'ant-design-vue';
|
|||
|
||||
import { HttpUtil, RandomUtil, SizeFormatter } from '@/utils';
|
||||
import DateTimePicker from '@/components/DateTimePicker.vue';
|
||||
import { DBInbound } from '@/models/dbinbound.js';
|
||||
import { TLS_FLOW_CONTROL } from '@/models/inbound.js';
|
||||
|
||||
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
|
||||
|
|
@ -46,10 +45,7 @@ const form = reactive({
|
|||
const flowCapableIds = computed(() => {
|
||||
const ids = new Set();
|
||||
for (const row of props.inbounds || []) {
|
||||
try {
|
||||
const parsed = new DBInbound(row).toInbound();
|
||||
if (parsed.canEnableTlsFlow?.()) ids.add(row.id);
|
||||
} catch (_e) { /* ignore */ }
|
||||
if (row?.tlsFlowCapable) ids.add(row.id);
|
||||
}
|
||||
return ids;
|
||||
});
|
||||
|
|
@ -176,9 +172,8 @@ async function submit() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal :open="open" :title="t('pages.clients.bulk') || 'Add Bulk'" :ok-text="t('create')"
|
||||
:cancel-text="t('close')" :confirm-loading="saving" :mask-closable="false" :width="640"
|
||||
@ok="submit" @cancel="close">
|
||||
<a-modal :open="open" :title="t('pages.clients.bulk') || 'Add Bulk'" :ok-text="t('create')" :cancel-text="t('close')"
|
||||
:confirm-loading="saving" :mask-closable="false" :width="640" @ok="submit" @cancel="close">
|
||||
<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-select v-model:value="form.inboundIds" mode="multiple" :options="inboundOptions"
|
||||
|
|
@ -231,11 +226,11 @@ async function submit() {
|
|||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('pages.clients.limitIp') || 'IP Limit'">
|
||||
<a-input-number v-model:value="form.limitIp" :min="0" :disabled="!ipLimitEnable" />
|
||||
<a-form-item v-if="ipLimitEnable" :label="t('pages.clients.limitIp') || 'IP Limit'">
|
||||
<a-input-number v-model:value="form.limitIp" :min="0" />
|
||||
</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-form-item>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n';
|
|||
import { message } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { HttpUtil, RandomUtil } from '@/utils';
|
||||
import { DBInbound } from '@/models/dbinbound.js';
|
||||
import { TLS_FLOW_CONTROL } from '@/models/inbound.js';
|
||||
|
||||
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
|
||||
|
|
@ -37,6 +36,7 @@ function emptyForm() {
|
|||
totalGB: 0,
|
||||
expiryTime: null,
|
||||
limitIp: 0,
|
||||
tgId: 0,
|
||||
comment: '',
|
||||
enable: true,
|
||||
inboundIds: [],
|
||||
|
|
@ -61,6 +61,7 @@ watch(
|
|||
form.totalGB = bytesToGB(props.client.totalGB || 0);
|
||||
form.expiryTime = props.client.expiryTime ? dayjs(props.client.expiryTime) : null;
|
||||
form.limitIp = props.client.limitIp || 0;
|
||||
form.tgId = Number(props.client.tgId) || 0;
|
||||
form.comment = props.client.comment || '';
|
||||
form.enable = !!props.client.enable;
|
||||
form.inboundIds = Array.isArray(props.attachedIds) ? [...props.attachedIds] : [];
|
||||
|
|
@ -102,10 +103,7 @@ const inboundOptions = computed(() =>
|
|||
const flowCapableIds = computed(() => {
|
||||
const ids = new Set();
|
||||
for (const row of props.inbounds || []) {
|
||||
try {
|
||||
const parsed = new DBInbound(row).toInbound();
|
||||
if (parsed.canEnableTlsFlow?.()) ids.add(row.id);
|
||||
} catch (_e) { /* ignore unparsable */ }
|
||||
if (row?.tlsFlowCapable) ids.add(row.id);
|
||||
}
|
||||
return ids;
|
||||
});
|
||||
|
|
@ -185,7 +183,7 @@ function regenerateSubId() {
|
|||
}
|
||||
|
||||
function regenerateEmail() {
|
||||
form.email = RandomUtil.randomLowerAndNum(9);
|
||||
form.email = RandomUtil.randomLowerAndNum(12);
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
|
|
@ -207,6 +205,7 @@ async function onSubmit() {
|
|||
totalGB: gbToBytes(form.totalGB),
|
||||
expiryTime: form.expiryTime ? form.expiryTime.valueOf() : 0,
|
||||
limitIp: Number(form.limitIp) || 0,
|
||||
tgId: Number(form.tgId) || 0,
|
||||
comment: form.comment,
|
||||
enable: !!form.enable,
|
||||
};
|
||||
|
|
@ -268,10 +267,10 @@ async function onSubmit() {
|
|||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="UUID">
|
||||
<a-form-item label="Auth (Hysteria)">
|
||||
<a-input-group compact style="display: flex">
|
||||
<a-input v-model:value="form.uuid" style="flex: 1" />
|
||||
<a-button @click="regenerateUUID">↻</a-button>
|
||||
<a-input v-model:value="form.auth" style="flex: 1" />
|
||||
<a-button @click="regenerateAuth">↻</a-button>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
|
@ -286,17 +285,17 @@ async function onSubmit() {
|
|||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="Auth (Hysteria)">
|
||||
<a-col :span="ipLimitEnable ? 12 : 24">
|
||||
<a-form-item label="UUID">
|
||||
<a-input-group compact style="display: flex">
|
||||
<a-input v-model:value="form.auth" style="flex: 1" />
|
||||
<a-button @click="regenerateAuth">↻</a-button>
|
||||
<a-input v-model:value="form.uuid" style="flex: 1" />
|
||||
<a-button @click="regenerateUUID">↻</a-button>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-col v-if="ipLimitEnable" :span="12">
|
||||
<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-col>
|
||||
</a-row>
|
||||
|
|
@ -330,9 +329,20 @@ async function onSubmit() {
|
|||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item :label="t('pages.clients.comment') || 'Comment'">
|
||||
<a-input v-model:value="form.comment" />
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<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-select v-model:value="form.inboundIds" mode="multiple" :options="inboundOptions" :show-search="true"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function useClients() {
|
|||
try {
|
||||
const [clientsMsg, inboundsMsg] = await Promise.all([
|
||||
HttpUtil.get('/panel/api/clients/list'),
|
||||
HttpUtil.get('/panel/api/inbounds/list'),
|
||||
HttpUtil.get('/panel/api/inbounds/options'),
|
||||
]);
|
||||
if (clientsMsg?.success) {
|
||||
clients.value = Array.isArray(clientsMsg.obj) ? clientsMsg.obj : [];
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ func (a *InboundController) broadcastInboundsUpdate(userId int) {
|
|||
func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||
|
||||
g.GET("/list", a.getInbounds)
|
||||
g.GET("/options", a.getInboundOptions)
|
||||
g.GET("/get/:id", a.getInbound)
|
||||
g.GET("/:id/fallbackChildren", a.getFallbackChildren)
|
||||
|
||||
|
|
@ -85,6 +86,19 @@ func (a *InboundController) getInbounds(c *gin.Context) {
|
|||
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.
|
||||
func (a *InboundController) getInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
|
|
|
|||
|
|
@ -134,6 +134,75 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
|
|||
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.
|
||||
func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ import (
|
|||
"github.com/mhsanaei/3x-ui/v3/web/locale"
|
||||
"github.com/mhsanaei/3x-ui/v3/xray"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mymmrac/telego"
|
||||
th "github.com/mymmrac/telego/telegohandler"
|
||||
tu "github.com/mymmrac/telego/telegoutil"
|
||||
|
|
@ -73,23 +72,23 @@ var (
|
|||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// clients data to adding new client
|
||||
receiver_inbound_ID int
|
||||
client_Id string
|
||||
client_Flow string
|
||||
client_Email string
|
||||
client_LimitIP int
|
||||
client_TotalGB int64
|
||||
client_ExpiryTime int64
|
||||
client_Enable bool
|
||||
client_TgID string
|
||||
client_SubID string
|
||||
client_Comment string
|
||||
client_Reset int
|
||||
client_Security string
|
||||
client_ShPassword string
|
||||
client_TrPassword string
|
||||
client_Method string
|
||||
// clients data to adding new client. receiver_inbound_IDs is the set of
|
||||
// inbounds the new client will be attached to; receiver_inbound_ID mirrors
|
||||
// the primary pick for the legacy attach-picker entry point. Per-protocol
|
||||
// secrets (UUID, password, flow, method) are filled per-inbound on submit
|
||||
// by ClientService.fillProtocolDefaults, so the bot only tracks universal
|
||||
// client fields here.
|
||||
receiver_inbound_ID int
|
||||
receiver_inbound_IDs []int
|
||||
client_Email string
|
||||
client_LimitIP int
|
||||
client_TotalGB int64
|
||||
client_ExpiryTime int64
|
||||
client_Enable bool
|
||||
client_TgID string
|
||||
client_SubID string
|
||||
client_Comment string
|
||||
client_Reset int
|
||||
)
|
||||
|
||||
var userStates = make(map[int64]string)
|
||||
|
|
@ -506,84 +505,6 @@ func (t *Tgbot) OnReceive() {
|
|||
h.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||
if userState, exists := userStates[message.Chat.ID]; exists {
|
||||
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":
|
||||
if client_Email == strings.TrimSpace(message.Text) {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
||||
|
|
@ -605,9 +526,7 @@ func (t *Tgbot) OnReceive() {
|
|||
} else {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_email"), 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)
|
||||
t.addClient(message.Chat.ID, t.BuildClientDraftMessage())
|
||||
}
|
||||
case "awaiting_comment":
|
||||
if client_Comment == strings.TrimSpace(message.Text) {
|
||||
|
|
@ -619,9 +538,29 @@ func (t *Tgbot) OnReceive() {
|
|||
client_Comment = strings.TrimSpace(message.Text)
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_comment"), 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)
|
||||
t.addClient(message.Chat.ID, t.BuildClientDraftMessage())
|
||||
case "awaiting_tg_id":
|
||||
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 {
|
||||
|
|
@ -749,16 +688,6 @@ func (t *Tgbot) randomLowerAndNum(length int) string {
|
|||
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.
|
||||
func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {
|
||||
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)
|
||||
client_TotalGB = limitTraffic * 1024 * 1024 * 1024
|
||||
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
|
||||
}
|
||||
message_text := t.BuildClientDraftMessage()
|
||||
|
||||
t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
message_text := t.BuildClientDraftMessage()
|
||||
|
||||
t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
|
||||
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()
|
||||
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
|
||||
}
|
||||
message_text := t.BuildClientDraftMessage()
|
||||
|
||||
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":
|
||||
|
|
@ -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)
|
||||
case "add_client_to":
|
||||
// assign default values to clients variables
|
||||
client_Id = uuid.New().String()
|
||||
client_Flow = ""
|
||||
client_Email = t.randomLowerAndNum(8)
|
||||
client_LimitIP = 0
|
||||
client_TotalGB = 0
|
||||
|
|
@ -1586,10 +1466,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
client_SubID = t.randomLowerAndNum(16)
|
||||
client_Comment = ""
|
||||
client_Reset = 0
|
||||
client_Security = "auto"
|
||||
client_ShPassword = t.randomShadowSocksPassword()
|
||||
client_TrPassword = t.randomLowerAndNum(10)
|
||||
client_Method = ""
|
||||
|
||||
inboundId := dataArray[1]
|
||||
inboundIdInt, err := strconv.Atoi(inboundId)
|
||||
|
|
@ -1598,19 +1474,33 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
return
|
||||
}
|
||||
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 {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
found := -1
|
||||
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 {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
t.addClient(callbackQuery.Message.GetChat().ID, message_text)
|
||||
t.editMessageCallbackTgBot(callbackQuery.Message.GetChat().ID, callbackQuery.Message.GetMessageID(), picker)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
|
|
@ -1753,9 +1643,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands"))
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpAdminCommands"))
|
||||
case "add_client":
|
||||
// assign default values to clients variables
|
||||
client_Id = uuid.New().String()
|
||||
client_Flow = ""
|
||||
client_Email = t.randomLowerAndNum(8)
|
||||
client_LimitIP = 0
|
||||
client_TotalGB = 0
|
||||
|
|
@ -1765,10 +1652,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
client_SubID = t.randomLowerAndNum(16)
|
||||
client_Comment = ""
|
||||
client_Reset = 0
|
||||
client_Security = "auto"
|
||||
client_ShPassword = t.randomShadowSocksPassword()
|
||||
client_TrPassword = t.randomLowerAndNum(10)
|
||||
client_Method = ""
|
||||
|
||||
inbounds, err := t.getInboundsAddClient()
|
||||
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)
|
||||
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":
|
||||
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
||||
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)
|
||||
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":
|
||||
inlineKeyboard := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
|
|
@ -1885,22 +1751,6 @@ 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(
|
||||
|
|
@ -1934,41 +1784,41 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
||||
t.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, chatId)
|
||||
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
t.addClient(chatId, message_text)
|
||||
t.addClient(chatId, t.BuildClientDraftMessage())
|
||||
case "add_client_cancel":
|
||||
delete(userStates, chatId)
|
||||
receiver_inbound_ID = 0
|
||||
receiver_inbound_IDs = nil
|
||||
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
||||
t.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot("tgbot.messages.cancel"), 3, tu.ReplyKeyboardRemove())
|
||||
case "add_client_default_traffic_exp":
|
||||
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
|
||||
}
|
||||
message_text := t.BuildClientDraftMessage()
|
||||
t.addClient(chatId, message_text, messageId)
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email))
|
||||
case "add_client_default_ip_limit":
|
||||
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
|
||||
}
|
||||
message_text := t.BuildClientDraftMessage()
|
||||
t.addClient(chatId, message_text, messageId)
|
||||
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":
|
||||
client_Enable = false
|
||||
_, 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.sendClientIndividualLinks(chatId, client_Email)
|
||||
t.sendClientQRLinks(chatId, client_Email)
|
||||
receiver_inbound_ID = 0
|
||||
receiver_inbound_IDs = nil
|
||||
}
|
||||
case "add_client_submit_enable":
|
||||
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.sendClientIndividualLinks(chatId, client_Email)
|
||||
t.sendClientQRLinks(chatId, client_Email)
|
||||
receiver_inbound_ID = 0
|
||||
receiver_inbound_IDs = nil
|
||||
}
|
||||
case "reset_all_traffics_cancel":
|
||||
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.
|
||||
func (t *Tgbot) BuildInboundClientDataMessage(inbound_remark string, protocol model.Protocol) (string, error) {
|
||||
var message string
|
||||
|
||||
currentTime := time.Now()
|
||||
timestampMillis := currentTime.UnixNano() / int64(time.Millisecond)
|
||||
|
||||
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)
|
||||
// BuildClientDraftMessage builds a protocol-neutral summary of the in-progress
|
||||
// client (email, attached inbounds, traffic limit, expiry, ip limit, comment)
|
||||
// shown in the multi-inbound add flow. Per-protocol secrets (UUID, password,
|
||||
// flow, method) are generated by fillProtocolDefaults on submit, so the bot
|
||||
// never has to track them per inbound itself.
|
||||
func (t *Tgbot) BuildClientDraftMessage() string {
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
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:
|
||||
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.
|
||||
func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) {
|
||||
var jsonString string
|
||||
|
||||
switch protocol {
|
||||
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")
|
||||
// describeAttachedInbounds returns a short "remark1, remark2" list for the given
|
||||
// inbound ids, falling back to "#id" when an inbound can't be loaded.
|
||||
func (t *Tgbot) describeAttachedInbounds(ids []int) string {
|
||||
if len(ids) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return jsonString, nil
|
||||
parts := make([]string, 0, len(ids))
|
||||
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) {
|
||||
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
if err != nil {
|
||||
logger.Warning("getIboundClients run failed:", err)
|
||||
inboundIDs := receiver_inbound_IDs
|
||||
if len(inboundIDs) == 0 && receiver_inbound_ID > 0 {
|
||||
inboundIDs = []int{receiver_inbound_ID}
|
||||
}
|
||||
if len(inboundIDs) == 0 {
|
||||
return false, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
|
||||
}
|
||||
|
||||
|
|
@ -2231,22 +2043,10 @@ func (t *Tgbot) SubmitAddClient() (bool, error) {
|
|||
TgID: tgIDInt,
|
||||
}
|
||||
|
||||
switch inbound.Protocol {
|
||||
case model.VMESS:
|
||||
client.ID = client_Id
|
||||
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)
|
||||
return t.clientService.Create(&t.inboundService, &ClientCreatePayload{
|
||||
Client: client,
|
||||
InboundIds: inboundIDs,
|
||||
})
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (t *Tgbot) getInboundUsages() string {
|
||||
var info strings.Builder
|
||||
// get traffic
|
||||
inbounds, err := t.inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
logger.Warning("GetAllInbounds run failed:", err)
|
||||
info.WriteString(t.I18nBot("tgbot.answers.getInboundsFailed"))
|
||||
} else {
|
||||
// NOTE:If there no any sessions here,need to notify here
|
||||
// TODO:Sub-node push, automatic conversion format
|
||||
for _, inbound := range inbounds {
|
||||
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.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)))
|
||||
return info.String()
|
||||
}
|
||||
for _, inbound := range inbounds {
|
||||
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.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)))
|
||||
|
||||
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")
|
||||
clients, listErr := t.clientService.ListForInbound(nil, inbound.Id)
|
||||
if listErr == nil {
|
||||
info.WriteString(fmt.Sprintf("👥 Clients: %d\r\n", len(clients)))
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
@ -3030,6 +2832,54 @@ func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
|
|||
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.
|
||||
func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error) {
|
||||
inbound, err := t.inboundService.GetInbound(id)
|
||||
|
|
@ -3143,6 +2993,9 @@ func (t *Tgbot) clientInfoMsg(
|
|||
|
||||
output := ""
|
||||
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 {
|
||||
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 {
|
||||
attachLabel := fmt.Sprintf("➕ Attach inbound (%d)", len(receiver_inbound_IDs))
|
||||
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.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.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.setTGUser")).WithCallbackData("add_client_ch_default_tg_id"),
|
||||
),
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(attachLabel).WithCallbackData("add_client_attach_more"),
|
||||
),
|
||||
tu.InlineKeyboardRow(
|
||||
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
|
||||
// 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.
|
||||
// addClient renders the draft message + shared client-first keyboard.
|
||||
func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
|
||||
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
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...)...)
|
||||
|
||||
inlineKeyboard := tu.InlineKeyboard(t.getCommonClientButtons()...)
|
||||
if len(messageID) > 0 {
|
||||
t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard)
|
||||
} else {
|
||||
t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// searchInbound searches for inbounds by remark and sends the results.
|
||||
|
|
|
|||
Loading…
Reference in a new issue