mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-07 05:34:17 +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:
|
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 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',
|
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}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 : [];
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue