2023-03-17 16:07:49 +00:00
package service
import (
2025-08-08 18:41:06 +00:00
"context"
2025-03-26 18:16:35 +00:00
"crypto/rand"
2023-05-20 15:38:01 +00:00
"embed"
2025-03-26 18:16:35 +00:00
"encoding/base64"
2026-02-11 21:21:09 +00:00
"encoding/json"
2024-08-18 21:30:56 +00:00
"errors"
2023-03-17 16:07:49 +00:00
"fmt"
2026-03-04 10:35:24 +00:00
"html"
2025-09-14 17:51:57 +00:00
"io"
2025-03-26 18:16:35 +00:00
"math/big"
2023-03-17 16:07:49 +00:00
"net"
2025-09-14 17:51:57 +00:00
"net/http"
2024-01-02 09:42:07 +00:00
"net/url"
2023-03-17 16:07:49 +00:00
"os"
2025-03-26 18:16:35 +00:00
"regexp"
2026-03-04 12:05:29 +00:00
"slices"
2023-03-17 16:07:49 +00:00
"strconv"
"strings"
2025-09-21 17:27:05 +00:00
"sync"
2023-03-17 16:07:49 +00:00
"time"
2024-03-10 21:31:24 +00:00
2026-05-10 00:13:42 +00:00
"github.com/mhsanaei/3x-ui/v3/config"
"github.com/mhsanaei/3x-ui/v3/database"
"github.com/mhsanaei/3x-ui/v3/database/model"
"github.com/mhsanaei/3x-ui/v3/logger"
"github.com/mhsanaei/3x-ui/v3/util/common"
"github.com/mhsanaei/3x-ui/v3/web/global"
"github.com/mhsanaei/3x-ui/v3/web/locale"
"github.com/mhsanaei/3x-ui/v3/xray"
2023-03-17 16:07:49 +00:00
2023-05-14 15:20:01 +00:00
"github.com/mymmrac/telego"
th "github.com/mymmrac/telego/telegohandler"
tu "github.com/mymmrac/telego/telegoutil"
2025-09-14 17:51:57 +00:00
"github.com/skip2/go-qrcode"
2024-01-02 09:42:07 +00:00
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttpproxy"
2023-03-17 16:07:49 +00:00
)
2024-03-10 21:31:24 +00:00
var (
2025-11-01 11:56:55 +00:00
bot * telego . Bot
// botCancel stores the function to cancel the context, stopping Long Polling gracefully.
botCancel context . CancelFunc
// tgBotMutex protects concurrent access to botCancel variable
tgBotMutex sync . Mutex
// botWG waits for the OnReceive Long Polling goroutine to finish.
botWG sync . WaitGroup
2025-04-06 22:45:52 +00:00
botHandler * th . BotHandler
adminIds [ ] int64
isRunning bool
hostname string
hashStorage * global . HashStorage
2025-03-26 18:16:35 +00:00
2025-09-21 17:27:05 +00:00
// Performance improvements
2025-09-21 22:20:05 +00:00
messageWorkerPool chan struct { } // Semaphore for limiting concurrent message processing
optimizedHTTPClient * http . Client // HTTP client with connection pooling and timeouts
2025-09-21 17:27:05 +00:00
// Simple cache for frequently accessed data
statusCache struct {
data * Status
timestamp time . Time
mutex sync . RWMutex
}
2025-09-21 22:20:05 +00:00
2025-09-21 17:27:05 +00:00
serverStatsCache struct {
data string
timestamp time . Time
mutex sync . RWMutex
}
2025-09-21 22:20:05 +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>
2026-05-18 00:11:30 +00:00
// 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
2024-03-10 21:31:24 +00:00
)
2023-03-17 16:07:49 +00:00
2025-03-26 18:16:35 +00:00
var userStates = make ( map [ int64 ] string )
2025-09-20 07:35:50 +00:00
// LoginStatus represents the result of a login attempt.
2023-03-17 16:07:49 +00:00
type LoginStatus byte
2025-09-20 07:35:50 +00:00
// Login status constants
2023-03-17 16:07:49 +00:00
const (
2025-09-20 07:35:50 +00:00
LoginSuccess LoginStatus = 1 // Login was successful
LoginFail LoginStatus = 0 // Login failed
EmptyTelegramUserID = int64 ( 0 ) // Default value for empty Telegram user ID
2023-03-17 16:07:49 +00:00
)
2026-05-07 21:36:11 +00:00
// LoginAttempt contains safe metadata for panel login notifications.
// It intentionally does not include attempted passwords.
type LoginAttempt struct {
Username string
IP string
Time string
Status LoginStatus
Reason string
}
2025-09-20 07:35:50 +00:00
// Tgbot provides business logic for Telegram bot integration.
// It handles bot commands, user interactions, and status reporting via Telegram.
2023-03-17 16:07:49 +00:00
type Tgbot struct {
inboundService InboundService
2026-05-17 08:29:25 +00:00
clientService ClientService
2023-03-17 16:07:49 +00:00
settingService SettingService
serverService ServerService
2023-05-04 23:17:26 +00:00
xrayService XrayService
2023-03-17 16:07:49 +00:00
lastStatus * Status
}
2025-09-20 07:35:50 +00:00
// NewTgbot creates a new Tgbot instance.
2023-03-17 16:07:49 +00:00
func ( t * Tgbot ) NewTgbot ( ) * Tgbot {
return new ( Tgbot )
}
2025-09-20 07:35:50 +00:00
// I18nBot retrieves a localized message for the bot interface.
2023-05-20 23:00:26 +00:00
func ( t * Tgbot ) I18nBot ( name string , params ... string ) string {
2023-05-20 15:38:01 +00:00
return locale . I18n ( locale . Bot , name , params ... )
}
2025-09-20 07:35:50 +00:00
// GetHashStorage returns the hash storage instance for callback queries.
2023-05-20 15:59:28 +00:00
func ( t * Tgbot ) GetHashStorage ( ) * global . HashStorage {
2023-05-21 01:03:01 +00:00
return hashStorage
2023-05-20 15:59:28 +00:00
}
2025-09-21 17:27:05 +00:00
// getCachedStatus returns cached server status if it's fresh enough (less than 5 seconds old)
func ( t * Tgbot ) getCachedStatus ( ) ( * Status , bool ) {
statusCache . mutex . RLock ( )
defer statusCache . mutex . RUnlock ( )
2025-09-21 22:20:05 +00:00
2025-09-21 17:27:05 +00:00
if statusCache . data != nil && time . Since ( statusCache . timestamp ) < 5 * time . Second {
return statusCache . data , true
}
return nil , false
}
// setCachedStatus updates the status cache
func ( t * Tgbot ) setCachedStatus ( status * Status ) {
statusCache . mutex . Lock ( )
defer statusCache . mutex . Unlock ( )
2025-09-21 22:20:05 +00:00
2025-09-21 17:27:05 +00:00
statusCache . data = status
statusCache . timestamp = time . Now ( )
}
// getCachedServerStats returns cached server stats if it's fresh enough (less than 10 seconds old)
func ( t * Tgbot ) getCachedServerStats ( ) ( string , bool ) {
serverStatsCache . mutex . RLock ( )
defer serverStatsCache . mutex . RUnlock ( )
2025-09-21 22:20:05 +00:00
2025-09-21 17:27:05 +00:00
if serverStatsCache . data != "" && time . Since ( serverStatsCache . timestamp ) < 10 * time . Second {
return serverStatsCache . data , true
}
return "" , false
}
// setCachedServerStats updates the server stats cache
func ( t * Tgbot ) setCachedServerStats ( stats string ) {
serverStatsCache . mutex . Lock ( )
defer serverStatsCache . mutex . Unlock ( )
2025-09-21 22:20:05 +00:00
2025-09-21 17:27:05 +00:00
serverStatsCache . data = stats
serverStatsCache . timestamp = time . Now ( )
}
2025-09-20 07:35:50 +00:00
// Start initializes and starts the Telegram bot with the provided translation files.
2023-05-20 15:38:01 +00:00
func ( t * Tgbot ) Start ( i18nFS embed . FS ) error {
2024-07-08 21:08:00 +00:00
// Initialize localizer
2023-05-20 15:38:01 +00:00
err := locale . InitLocalizer ( i18nFS , & t . settingService )
if err != nil {
return err
}
2026-01-02 15:13:32 +00:00
// If Start is called again (e.g. during reload), ensure any previous long-polling
// loop is stopped before creating a new bot / receiver.
StopBot ( )
2024-07-08 21:08:00 +00:00
// Initialize hash storage to store callback queries
2023-05-21 04:03:08 +00:00
hashStorage = global . NewHashStorage ( 20 * time . Minute )
2023-05-20 15:59:28 +00:00
2025-09-21 17:27:05 +00:00
// Initialize worker pool for concurrent message processing (max 10 concurrent handlers)
messageWorkerPool = make ( chan struct { } , 10 )
2025-09-21 22:20:05 +00:00
2025-09-21 17:27:05 +00:00
// Initialize optimized HTTP client with connection pooling
optimizedHTTPClient = & http . Client {
Timeout : 15 * time . Second ,
Transport : & http . Transport {
MaxIdleConns : 100 ,
MaxIdleConnsPerHost : 10 ,
IdleConnTimeout : 30 * time . Second ,
DisableKeepAlives : false ,
} ,
}
2023-05-20 23:00:26 +00:00
t . SetHostname ( )
2024-07-08 21:08:00 +00:00
// Get Telegram bot token
tgBotToken , err := t . settingService . GetTgBotToken ( )
if err != nil || tgBotToken == "" {
logger . Warning ( "Failed to get Telegram bot token:" , err )
2023-03-17 16:07:49 +00:00
return err
}
2024-07-08 21:08:00 +00:00
// Get Telegram bot chat ID(s)
tgBotID , err := t . settingService . GetTgBotChatId ( )
2023-03-17 16:07:49 +00:00
if err != nil {
2024-07-08 21:08:00 +00:00
logger . Warning ( "Failed to get Telegram bot chat ID:" , err )
2023-03-17 16:07:49 +00:00
return err
}
2026-01-02 15:13:32 +00:00
parsedAdminIds := make ( [ ] int64 , 0 )
2024-07-08 21:08:00 +00:00
// Parse admin IDs from comma-separated string
if tgBotID != "" {
for _ , adminID := range strings . Split ( tgBotID , "," ) {
2025-12-03 13:58:54 +00:00
id , err := strconv . ParseInt ( adminID , 10 , 64 )
2023-05-31 01:31:20 +00:00
if err != nil {
2024-07-08 21:08:00 +00:00
logger . Warning ( "Failed to parse admin ID from Telegram bot chat ID:" , err )
2023-05-31 01:31:20 +00:00
return err
}
2026-01-02 15:13:32 +00:00
parsedAdminIds = append ( parsedAdminIds , int64 ( id ) )
2023-03-17 16:07:49 +00:00
}
}
2026-01-02 15:13:32 +00:00
tgBotMutex . Lock ( )
adminIds = parsedAdminIds
tgBotMutex . Unlock ( )
2023-03-17 16:07:49 +00:00
2024-07-08 21:08:00 +00:00
// Get Telegram bot proxy URL
2024-01-02 09:42:07 +00:00
tgBotProxy , err := t . settingService . GetTgBotProxy ( )
if err != nil {
2024-07-08 21:08:00 +00:00
logger . Warning ( "Failed to get Telegram bot proxy URL:" , err )
2024-01-02 09:42:07 +00:00
}
2024-10-17 08:59:42 +00:00
// Get Telegram bot API server URL
tgBotAPIServer , err := t . settingService . GetTgBotAPIServer ( )
if err != nil {
logger . Warning ( "Failed to get Telegram bot API server URL:" , err )
}
2024-07-08 21:08:00 +00:00
// Create new Telegram bot instance
2024-10-17 08:59:42 +00:00
bot , err = t . NewBot ( tgBotToken , tgBotProxy , tgBotAPIServer )
2023-03-17 16:07:49 +00:00
if err != nil {
2024-07-08 21:08:00 +00:00
logger . Error ( "Failed to initialize Telegram bot API:" , err )
2023-03-17 16:07:49 +00:00
return err
}
2026-04-22 08:47:30 +00:00
t . trySetBotCommands ( bot )
2025-05-28 08:26:29 +00:00
2024-07-08 21:08:00 +00:00
// Start receiving Telegram bot messages
2026-01-02 15:13:32 +00:00
tgBotMutex . Lock ( )
alreadyRunning := isRunning || botCancel != nil
tgBotMutex . Unlock ( )
if ! alreadyRunning {
2024-07-08 21:08:00 +00:00
logger . Info ( "Telegram bot receiver started" )
2023-03-17 16:07:49 +00:00
go t . OnReceive ( )
}
return nil
}
2026-04-22 08:47:30 +00:00
func ( t * Tgbot ) trySetBotCommands ( bot * telego . Bot ) {
defer func ( ) {
if r := recover ( ) ; r != nil {
logger . Warning ( "Failed to register bot commands (Telegram may be rate-limiting); bot will continue without them:" , r )
}
} ( )
err := bot . SetMyCommands ( context . Background ( ) , & telego . SetMyCommandsParams {
Commands : [ ] telego . BotCommand {
{ Command : "start" , Description : t . I18nBot ( "tgbot.commands.startDesc" ) } ,
{ Command : "help" , Description : t . I18nBot ( "tgbot.commands.helpDesc" ) } ,
{ Command : "status" , Description : t . I18nBot ( "tgbot.commands.statusDesc" ) } ,
{ Command : "id" , Description : t . I18nBot ( "tgbot.commands.idDesc" ) } ,
} ,
} )
if err != nil {
logger . Warning ( "Failed to set bot commands:" , err )
}
}
2026-02-14 21:49:19 +00:00
// createRobustFastHTTPClient creates a fasthttp.Client with proper connection handling
func ( t * Tgbot ) createRobustFastHTTPClient ( proxyUrl string ) * fasthttp . Client {
client := & fasthttp . Client {
// Connection timeouts
ReadTimeout : 30 * time . Second ,
WriteTimeout : 30 * time . Second ,
MaxIdleConnDuration : 60 * time . Second ,
MaxConnDuration : 0 , // unlimited, but controlled by MaxIdleConnDuration
MaxIdemponentCallAttempts : 3 ,
ReadBufferSize : 4096 ,
WriteBufferSize : 4096 ,
MaxConnsPerHost : 100 ,
MaxConnWaitTimeout : 10 * time . Second ,
DisableHeaderNamesNormalizing : false ,
DisablePathNormalizing : false ,
// Retry on connection errors
RetryIf : func ( request * fasthttp . Request ) bool {
// Retry on connection errors for GET requests
return string ( request . Header . Method ( ) ) == "GET" || string ( request . Header . Method ( ) ) == "POST"
} ,
}
// Set proxy if provided
if proxyUrl != "" {
client . Dial = fasthttpproxy . FasthttpSocksDialer ( proxyUrl )
2024-02-03 14:45:47 +00:00
}
2026-02-14 21:49:19 +00:00
return client
}
// NewBot creates a new Telegram bot instance with optional proxy and API server settings.
func ( t * Tgbot ) NewBot ( token string , proxyUrl string , apiServerUrl string ) ( * telego . Bot , error ) {
// Validate proxy URL if provided
2024-10-17 08:59:42 +00:00
if proxyUrl != "" {
if ! strings . HasPrefix ( proxyUrl , "socks5://" ) {
2026-02-14 21:49:19 +00:00
logger . Warning ( "Invalid socks5 URL, ignoring proxy" )
proxyUrl = "" // Clear invalid proxy
} else {
_ , err := url . Parse ( proxyUrl )
if err != nil {
logger . Warningf ( "Can't parse proxy URL, ignoring proxy: %v" , err )
proxyUrl = ""
}
2024-10-17 08:59:42 +00:00
}
2026-02-14 21:49:19 +00:00
}
2024-10-17 08:59:42 +00:00
2026-02-14 21:49:19 +00:00
// Validate API server URL if provided
if apiServerUrl != "" {
Security hardening: sessions, SSRF, CSP nonce, CSRF logout, trusted proxies (#4275)
* refactor(session): store user ID in session instead of full struct
Replaces storing the full User object in the session cookie with just
the user ID. GetLoginUser now re-fetches the user from the database on
every request so credential/permission changes take effect immediately
without requiring a re-login. Includes a backward-compatible migration
path for existing sessions that still carry the old struct payload.
* feat(auth): block panel with default admin/admin credentials and guide credential change
checkLogin middleware now detects default admin/admin credentials and
redirects every panel route to /panel/settings until they are changed.
The settings page auto-opens the Authentication tab, shows a
non-dismissible error banner, and lists 'Default credentials' first in
the security checklist. Login response includes mustChangeCredentials
so the login page can redirect directly. Logout is now POST-only.
Password must be at least 10 characters and cannot be admin/admin.
* feat(settings): redact secrets in AllSettingView and add TrustedProxyCIDRs
Introduces AllSettingView which strips tgBotToken, twoFactorToken,
ldapPassword, apiToken and warp/nord secrets before sending them to
the browser, replacing them with boolean hasFoo presence flags. A new
/panel/setting/secret endpoint allows updating individual secrets by
key. Secrets that arrive blank on a save are preserved from the DB
rather than overwritten. Adds TrustedProxyCIDRs as a configurable
setting (defaults to localhost CIDRs). URL fields are validated before
save.
* fix(security): SSRF prevention, trusted-proxy header gating, CSP nonce, HTTP timeouts
Adds SanitizeHTTPURL / SanitizePublicHTTPURL to reject private-range
and loopback targets before any outbound HTTP request (node probe,
xray download, outbound test, external traffic inform, tgbot API
server, panel updater). Forwarded headers (X-Real-IP, X-Forwarded-For,
X-Forwarded-Host) are now only trusted when the direct connection
arrives from a CIDR in TrustedProxyCIDRs. CSP policy is tightened with
a per-request nonce. HTTP server gains read/write/idle timeouts. Panel
updater downloads the script to a temp file instead of piping curl into
shell. Xray archive download adds a size cap and response-code check.
backuptotgbot is changed from GET to POST.
* feat(nodes): add allow-private-address toggle per node
Adds AllowPrivateAddress to the Node model (DB default false). When
enabled it bypasses the SSRF private-range check for that node's probe
URL, allowing nodes hosted on RFC-1918 or loopback addresses (e.g.
a private VPN or LAN setup).
* chore: frontend UX improvements, CI pipeline, and dev tooling
- AppSidebar: logout via POST /logout instead of navigating to GET
- InboundList: persist filter state (search, protocol, node) to
localStorage across page reloads; add protocol and node filter dropdowns
- IndexPage: add health status strip (Xray, CPU, Memory, Update) with
quick-action buttons
- dependabot: weekly go mod and npm update schedule
- ci.yml: add GitHub Actions workflow for build and vet
- .nvmrc: pin Node 22 for local development
- frontend: bump package.json and package-lock.json
- SubPage, DnsPresetsModal, api-docs: minor fixes
* fix(ci): stub web/dist before go list to satisfy go:embed at compile time
* chore(ui): remove health-strip bar from dashboard top
* Revert "feat(auth): block panel with default admin/admin credentials and guide credential change"
This reverts commit 56ce6073ce09f08147f989858e0e88b3a4359546.
* fix(auth): make logout POST+CSRF and propagate session loss to other tabs
- Switch /logout from GET to POST with CSRFMiddleware so it matches the
SPA's existing HttpUtil.post('/logout') call (previously 404'd silently)
and blocks GET-based logout via image tags or link prefetchers. Handler
now returns JSON; the SPA already navigates client-side.
- Return 401 (instead of 404) from /panel/api/* when the caller is a
browser XHR (X-Requested-With: XMLHttpRequest) so the axios interceptor
redirects to the login page on logout-in-another-tab, cookie expiry,
and server restart. Anonymous callers still get 404 to keep endpoints
hidden from casual scanners.
- One-shot the 401 redirect in axios-init.js and hang the rejected
promise so queued polls don't stack reloads or surface error toasts
while the browser is navigating away.
- Add the CSP nonce to the runtime-injected <script> in dist.go so the
panel loads under the existing script-src 'nonce-...' policy.
- Update api-docs endpoints.js: GET /logout doc entry was missing.
* fix(settings): POST /logout after credential change
* fix(auth): invalidate other sessions when credentials change
When the admin changes username/password from one machine, sessions
on every other machine kept working until they manually logged out
because session storage is a signed client-side cookie — there is
no server-side session list to revoke.
Add a per-user LoginEpoch counter stamped into the session at login
and re-verified on every authenticated request. UpdateUser and
UpdateFirstUser bump the epoch (UpdateUser via gorm.Expr so a single
update statement is atomic), so any cookie issued before the change
no longer matches the user's current epoch and GetLoginUser returns
nil — the SPA's 401 interceptor then redirects to the login page.
Backward compatible: the column defaults to 0 and missing cookie
values are treated as 0, so sessions issued before this change
remain valid until the first credential update.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-05-13 10:52:52 +00:00
safeURL , err := SanitizePublicHTTPURL ( apiServerUrl , false )
if err != nil {
logger . Warningf ( "Invalid or blocked API server URL, using default: %v" , err )
2026-02-14 21:49:19 +00:00
apiServerUrl = ""
} else {
Security hardening: sessions, SSRF, CSP nonce, CSRF logout, trusted proxies (#4275)
* refactor(session): store user ID in session instead of full struct
Replaces storing the full User object in the session cookie with just
the user ID. GetLoginUser now re-fetches the user from the database on
every request so credential/permission changes take effect immediately
without requiring a re-login. Includes a backward-compatible migration
path for existing sessions that still carry the old struct payload.
* feat(auth): block panel with default admin/admin credentials and guide credential change
checkLogin middleware now detects default admin/admin credentials and
redirects every panel route to /panel/settings until they are changed.
The settings page auto-opens the Authentication tab, shows a
non-dismissible error banner, and lists 'Default credentials' first in
the security checklist. Login response includes mustChangeCredentials
so the login page can redirect directly. Logout is now POST-only.
Password must be at least 10 characters and cannot be admin/admin.
* feat(settings): redact secrets in AllSettingView and add TrustedProxyCIDRs
Introduces AllSettingView which strips tgBotToken, twoFactorToken,
ldapPassword, apiToken and warp/nord secrets before sending them to
the browser, replacing them with boolean hasFoo presence flags. A new
/panel/setting/secret endpoint allows updating individual secrets by
key. Secrets that arrive blank on a save are preserved from the DB
rather than overwritten. Adds TrustedProxyCIDRs as a configurable
setting (defaults to localhost CIDRs). URL fields are validated before
save.
* fix(security): SSRF prevention, trusted-proxy header gating, CSP nonce, HTTP timeouts
Adds SanitizeHTTPURL / SanitizePublicHTTPURL to reject private-range
and loopback targets before any outbound HTTP request (node probe,
xray download, outbound test, external traffic inform, tgbot API
server, panel updater). Forwarded headers (X-Real-IP, X-Forwarded-For,
X-Forwarded-Host) are now only trusted when the direct connection
arrives from a CIDR in TrustedProxyCIDRs. CSP policy is tightened with
a per-request nonce. HTTP server gains read/write/idle timeouts. Panel
updater downloads the script to a temp file instead of piping curl into
shell. Xray archive download adds a size cap and response-code check.
backuptotgbot is changed from GET to POST.
* feat(nodes): add allow-private-address toggle per node
Adds AllowPrivateAddress to the Node model (DB default false). When
enabled it bypasses the SSRF private-range check for that node's probe
URL, allowing nodes hosted on RFC-1918 or loopback addresses (e.g.
a private VPN or LAN setup).
* chore: frontend UX improvements, CI pipeline, and dev tooling
- AppSidebar: logout via POST /logout instead of navigating to GET
- InboundList: persist filter state (search, protocol, node) to
localStorage across page reloads; add protocol and node filter dropdowns
- IndexPage: add health status strip (Xray, CPU, Memory, Update) with
quick-action buttons
- dependabot: weekly go mod and npm update schedule
- ci.yml: add GitHub Actions workflow for build and vet
- .nvmrc: pin Node 22 for local development
- frontend: bump package.json and package-lock.json
- SubPage, DnsPresetsModal, api-docs: minor fixes
* fix(ci): stub web/dist before go list to satisfy go:embed at compile time
* chore(ui): remove health-strip bar from dashboard top
* Revert "feat(auth): block panel with default admin/admin credentials and guide credential change"
This reverts commit 56ce6073ce09f08147f989858e0e88b3a4359546.
* fix(auth): make logout POST+CSRF and propagate session loss to other tabs
- Switch /logout from GET to POST with CSRFMiddleware so it matches the
SPA's existing HttpUtil.post('/logout') call (previously 404'd silently)
and blocks GET-based logout via image tags or link prefetchers. Handler
now returns JSON; the SPA already navigates client-side.
- Return 401 (instead of 404) from /panel/api/* when the caller is a
browser XHR (X-Requested-With: XMLHttpRequest) so the axios interceptor
redirects to the login page on logout-in-another-tab, cookie expiry,
and server restart. Anonymous callers still get 404 to keep endpoints
hidden from casual scanners.
- One-shot the 401 redirect in axios-init.js and hang the rejected
promise so queued polls don't stack reloads or surface error toasts
while the browser is navigating away.
- Add the CSP nonce to the runtime-injected <script> in dist.go so the
panel loads under the existing script-src 'nonce-...' policy.
- Update api-docs endpoints.js: GET /logout doc entry was missing.
* fix(settings): POST /logout after credential change
* fix(auth): invalidate other sessions when credentials change
When the admin changes username/password from one machine, sessions
on every other machine kept working until they manually logged out
because session storage is a signed client-side cookie — there is
no server-side session list to revoke.
Add a per-user LoginEpoch counter stamped into the session at login
and re-verified on every authenticated request. UpdateUser and
UpdateFirstUser bump the epoch (UpdateUser via gorm.Expr so a single
update statement is atomic), so any cookie issued before the change
no longer matches the user's current epoch and GetLoginUser returns
nil — the SPA's 401 interceptor then redirects to the login page.
Backward compatible: the column defaults to 0 and missing cookie
values are treated as 0, so sessions issued before this change
remain valid until the first credential update.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-05-13 10:52:52 +00:00
apiServerUrl = safeURL
2024-10-17 08:59:42 +00:00
}
}
2026-02-14 21:49:19 +00:00
// Create robust fasthttp client
client := t . createRobustFastHTTPClient ( proxyUrl )
2024-01-02 09:42:07 +00:00
2026-02-14 21:49:19 +00:00
// Build bot options
var options [ ] telego . BotOption
options = append ( options , telego . WithFastHTTPClient ( client ) )
if apiServerUrl != "" {
options = append ( options , telego . WithAPIServer ( apiServerUrl ) )
2024-01-02 09:42:07 +00:00
}
2026-02-14 21:49:19 +00:00
return telego . NewBot ( token , options ... )
2024-01-02 09:42:07 +00:00
}
2025-09-20 07:35:50 +00:00
// IsRunning checks if the Telegram bot is currently running.
2023-05-20 15:09:01 +00:00
func ( t * Tgbot ) IsRunning ( ) bool {
2026-01-02 15:13:32 +00:00
tgBotMutex . Lock ( )
defer tgBotMutex . Unlock ( )
2023-03-17 16:07:49 +00:00
return isRunning
}
2025-09-20 07:35:50 +00:00
// SetHostname sets the hostname for the bot.
2023-05-20 23:00:26 +00:00
func ( t * Tgbot ) SetHostname ( ) {
host , err := os . Hostname ( )
if err != nil {
logger . Error ( "get hostname error:" , err )
hostname = ""
return
}
hostname = host
}
2025-11-01 11:56:55 +00:00
// Stop safely stops the Telegram bot's Long Polling operation.
// This method now calls the global StopBot function and cleans up other resources.
2023-03-17 16:07:49 +00:00
func ( t * Tgbot ) Stop ( ) {
2025-11-01 11:56:55 +00:00
StopBot ( )
2023-03-17 16:07:49 +00:00
logger . Info ( "Stop Telegram receiver ..." )
2026-01-02 15:13:32 +00:00
tgBotMutex . Lock ( )
2023-03-17 16:07:49 +00:00
adminIds = nil
2026-01-02 15:13:32 +00:00
tgBotMutex . Unlock ( )
2023-03-17 16:07:49 +00:00
}
2025-11-01 11:56:55 +00:00
// StopBot safely stops the Telegram bot's Long Polling operation by cancelling its context.
// This is the global function called from main.go's signal handler and t.Stop().
func StopBot ( ) {
2026-01-02 15:13:32 +00:00
// Don't hold the mutex while cancelling/waiting.
2025-11-01 11:56:55 +00:00
tgBotMutex . Lock ( )
2026-01-02 15:13:32 +00:00
cancel := botCancel
botCancel = nil
handler := botHandler
botHandler = nil
isRunning = false
tgBotMutex . Unlock ( )
2025-11-01 11:56:55 +00:00
2026-01-02 15:13:32 +00:00
if handler != nil {
handler . Stop ( )
}
2025-11-01 11:56:55 +00:00
2026-01-02 15:13:32 +00:00
if cancel != nil {
logger . Info ( "Sending cancellation signal to Telegram bot..." )
// Cancels the context passed to UpdatesViaLongPolling; this closes updates channel
// and lets botHandler.Start() exit cleanly.
cancel ( )
2025-11-01 11:56:55 +00:00
botWG . Wait ( )
logger . Info ( "Telegram bot successfully stopped." )
}
}
2025-09-20 07:35:50 +00:00
// encodeQuery encodes the query string if it's longer than 64 characters.
2023-05-21 04:03:08 +00:00
func ( t * Tgbot ) encodeQuery ( query string ) string {
// NOTE: we only need to hash for more than 64 chars
if len ( query ) <= 64 {
return query
}
return hashStorage . SaveHash ( query )
}
2025-09-20 07:35:50 +00:00
// decodeQuery decodes a hashed query string back to its original form.
2023-05-21 04:03:08 +00:00
func ( t * Tgbot ) decodeQuery ( query string ) ( string , error ) {
if ! hashStorage . IsMD5 ( query ) {
return query , nil
}
decoded , exists := hashStorage . GetValue ( query )
if ! exists {
return "" , common . NewError ( "hash not found in storage!" )
}
return decoded , nil
}
2025-09-20 07:35:50 +00:00
// OnReceive starts the message receiving loop for the Telegram bot.
2023-03-17 16:07:49 +00:00
func ( t * Tgbot ) OnReceive ( ) {
2023-05-14 15:20:01 +00:00
params := telego . GetUpdatesParams {
2026-02-14 21:49:19 +00:00
Timeout : 20 , // Reduced timeout to detect connection issues faster
2023-03-17 16:07:49 +00:00
}
2026-01-02 15:13:32 +00:00
// Strict singleton: never start a second long-polling loop.
2025-11-01 11:56:55 +00:00
tgBotMutex . Lock ( )
2026-01-02 15:13:32 +00:00
if botCancel != nil || isRunning {
tgBotMutex . Unlock ( )
logger . Warning ( "TgBot OnReceive called while already running; ignoring." )
return
2025-11-01 11:56:55 +00:00
}
2023-05-14 15:20:01 +00:00
2026-01-02 15:13:32 +00:00
ctx , cancel := context . WithCancel ( context . Background ( ) )
botCancel = cancel
isRunning = true
// Add to WaitGroup before releasing the lock so StopBot() can't return
// before this receiver goroutine is accounted for.
botWG . Add ( 1 )
2025-11-01 11:56:55 +00:00
tgBotMutex . Unlock ( )
2023-05-14 18:37:49 +00:00
2026-02-14 21:49:19 +00:00
// Get updates channel using the context with shorter timeout for better error recovery
2025-11-01 11:56:55 +00:00
updates , _ := bot . UpdatesViaLongPolling ( ctx , & params )
2026-01-02 15:13:32 +00:00
go func ( ) {
defer botWG . Done ( )
h , _ := th . NewBotHandler ( bot , updates )
tgBotMutex . Lock ( )
botHandler = h
tgBotMutex . Unlock ( )
h . HandleMessage ( func ( ctx * th . Context , message telego . Message ) error {
2025-09-21 17:27:05 +00:00
delete ( userStates , message . Chat . ID )
2025-11-01 11:56:55 +00:00
t . SendMsgToTgbot ( message . Chat . ID , t . I18nBot ( "tgbot.keyboardClosed" ) , tu . ReplyKeyboardRemove ( ) )
return nil
} , th . TextEqual ( t . I18nBot ( "tgbot.buttons.closeKeyboard" ) ) )
2025-03-26 18:16:35 +00:00
2026-01-02 15:13:32 +00:00
h . HandleMessage ( func ( ctx * th . Context , message telego . Message ) error {
2025-11-01 11:56:55 +00:00
// Use goroutine with worker pool for concurrent command processing
go func ( ) {
messageWorkerPool <- struct { } { } // Acquire worker
defer func ( ) { <- messageWorkerPool } ( ) // Release worker
2025-03-26 18:16:35 +00:00
2025-11-01 11:56:55 +00:00
delete ( userStates , message . Chat . ID )
t . answerCommand ( & message , message . Chat . ID , checkAdmin ( message . From . ID ) )
} ( )
return nil
} , th . AnyCommand ( ) )
2026-01-02 15:13:32 +00:00
h . HandleCallbackQuery ( func ( ctx * th . Context , query telego . CallbackQuery ) error {
2025-11-01 11:56:55 +00:00
// Use goroutine with worker pool for concurrent callback processing
go func ( ) {
messageWorkerPool <- struct { } { } // Acquire worker
defer func ( ) { <- messageWorkerPool } ( ) // Release worker
delete ( userStates , query . Message . GetChat ( ) . ID )
t . answerCallback ( & query , checkAdmin ( query . From . ID ) )
} ( )
return nil
} , th . AnyCallbackQueryWithMessage ( ) )
2026-01-02 15:13:32 +00:00
h . HandleMessage ( func ( ctx * th . Context , message telego . Message ) error {
2025-11-01 11:56:55 +00:00
if userState , exists := userStates [ message . Chat . ID ] ; exists {
switch userState {
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>
2026-05-18 00:11:30 +00:00
case "awaiting_email" :
if client_Email == strings . TrimSpace ( message . Text ) {
2025-11-01 11:56:55 +00:00
t . SendMsgToTgbotDeleteAfter ( message . Chat . ID , t . I18nBot ( "tgbot.messages.using_default_value" ) , 3 , tu . ReplyKeyboardRemove ( ) )
delete ( userStates , message . Chat . ID )
return nil
}
2025-03-26 18:16:35 +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>
2026-05-18 00:11:30 +00:00
client_Email = strings . TrimSpace ( message . Text )
if t . isSingleWord ( client_Email ) {
userStates [ message . Chat . ID ] = "awaiting_email"
2025-11-01 11:56:55 +00:00
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 {
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>
2026-05-18 00:11:30 +00:00
t . SendMsgToTgbotDeleteAfter ( message . Chat . ID , t . I18nBot ( "tgbot.messages.received_email" ) , 3 , tu . ReplyKeyboardRemove ( ) )
2025-11-01 11:56:55 +00:00
delete ( userStates , message . Chat . ID )
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>
2026-05-18 00:11:30 +00:00
t . addClient ( message . Chat . ID , t . BuildClientDraftMessage ( ) )
2025-11-01 11:56:55 +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>
2026-05-18 00:11:30 +00:00
case "awaiting_comment" :
if client_Comment == strings . TrimSpace ( message . Text ) {
2025-11-01 11:56:55 +00:00
t . SendMsgToTgbotDeleteAfter ( message . Chat . ID , t . I18nBot ( "tgbot.messages.using_default_value" ) , 3 , tu . ReplyKeyboardRemove ( ) )
delete ( userStates , message . Chat . ID )
return nil
}
2025-03-26 18:16:35 +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>
2026-05-18 00:11:30 +00:00
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 )
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 = ""
2025-11-01 11:56:55 +00:00
t . SendMsgToTgbotDeleteAfter ( message . Chat . ID , t . I18nBot ( "tgbot.messages.using_default_value" ) , 3 , tu . ReplyKeyboardRemove ( ) )
delete ( userStates , message . Chat . ID )
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>
2026-05-18 00:11:30 +00:00
t . addClient ( message . Chat . ID , t . BuildClientDraftMessage ( ) )
2025-11-01 11:56:55 +00:00
return nil
}
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>
2026-05-18 00:11:30 +00:00
if _ , err := strconv . ParseInt ( input , 10 , 64 ) ; err != nil {
2025-11-01 11:56:55 +00:00
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
}
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>
2026-05-18 00:11:30 +00:00
client_TgID = input
t . SendMsgToTgbotDeleteAfter ( message . Chat . ID , t . I18nBot ( "tgbot.messages.userSaved" ) , 3 , tu . ReplyKeyboardRemove ( ) )
2025-03-26 18:16:35 +00:00
delete ( userStates , message . Chat . ID )
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>
2026-05-18 00:11:30 +00:00
t . addClient ( message . Chat . ID , t . BuildClientDraftMessage ( ) )
2025-03-26 18:16:35 +00:00
}
2025-11-01 11:56:55 +00:00
} else {
if message . UsersShared != nil {
if checkAdmin ( message . From . ID ) {
for _ , sharedUser := range message . UsersShared . Users {
userID := sharedUser . UserID
refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 08:48:28 +00:00
needRestart , err := t . clientService . SetClientTelegramUserID ( & t . inboundService , message . UsersShared . RequestID , userID )
2025-11-01 11:56:55 +00:00
if needRestart {
t . xrayService . SetToNeedRestart ( )
}
output := ""
if err != nil {
output += t . I18nBot ( "tgbot.messages.selectUserFailed" )
} else {
output += t . I18nBot ( "tgbot.messages.userSaved" )
}
t . SendMsgToTgbot ( message . Chat . ID , output , tu . ReplyKeyboardRemove ( ) )
2025-03-26 18:16:35 +00:00
}
2025-11-01 11:56:55 +00:00
} else {
t . SendMsgToTgbot ( message . Chat . ID , t . I18nBot ( "tgbot.noResult" ) , tu . ReplyKeyboardRemove ( ) )
2024-04-02 11:34:44 +00:00
}
2023-05-14 18:37:49 +00:00
}
}
2025-11-01 11:56:55 +00:00
return nil
} , th . AnyMessage ( ) )
2023-05-14 18:37:49 +00:00
2026-01-02 15:13:32 +00:00
h . Start ( )
} ( )
2023-03-17 16:07:49 +00:00
}
2025-09-20 07:35:50 +00:00
// answerCommand processes incoming command messages from Telegram users.
2023-05-14 15:20:01 +00:00
func ( t * Tgbot ) answerCommand ( message * telego . Message , chatId int64 , isAdmin bool ) {
2023-05-30 22:01:00 +00:00
msg , onlyMessage := "" , false
2023-05-14 15:20:01 +00:00
2023-10-25 19:33:17 +00:00
command , _ , commandArgs := tu . ParseCommand ( message . Text )
2023-05-14 15:20:01 +00:00
2024-10-16 12:39:25 +00:00
// Helper function to handle unknown commands.
handleUnknownCommand := func ( ) {
msg += t . I18nBot ( "tgbot.commands.unknown" )
}
// Handle the command.
2023-05-14 15:20:01 +00:00
switch command {
2023-03-17 16:07:49 +00:00
case "help" :
2023-05-20 23:00:26 +00:00
msg += t . I18nBot ( "tgbot.commands.help" )
msg += t . I18nBot ( "tgbot.commands.pleaseChoose" )
2023-03-17 16:07:49 +00:00
case "start" :
2026-03-04 10:35:24 +00:00
msg += t . I18nBot ( "tgbot.commands.start" , "Firstname==" + html . EscapeString ( message . From . FirstName ) )
2023-03-17 16:07:49 +00:00
if isAdmin {
2023-05-20 23:00:26 +00:00
msg += t . I18nBot ( "tgbot.commands.welcome" , "Hostname==" + hostname )
2023-03-17 16:07:49 +00:00
}
2023-05-20 23:00:26 +00:00
msg += "\n\n" + t . I18nBot ( "tgbot.commands.pleaseChoose" )
2023-03-17 16:07:49 +00:00
case "status" :
2023-05-30 22:01:00 +00:00
onlyMessage = true
2023-05-20 23:00:26 +00:00
msg += t . I18nBot ( "tgbot.commands.status" )
2023-05-30 22:01:00 +00:00
case "id" :
onlyMessage = true
msg += t . I18nBot ( "tgbot.commands.getID" , "ID==" + strconv . FormatInt ( message . From . ID , 10 ) )
2023-03-17 16:07:49 +00:00
case "usage" :
2023-05-30 22:01:00 +00:00
onlyMessage = true
2023-05-14 15:20:01 +00:00
if len ( commandArgs ) > 0 {
2023-03-24 13:10:56 +00:00
if isAdmin {
2023-05-14 15:20:01 +00:00
t . searchClient ( chatId , commandArgs [ 0 ] )
2023-03-24 13:10:56 +00:00
} else {
2024-10-16 12:39:25 +00:00
t . getClientUsage ( chatId , int64 ( message . From . ID ) , commandArgs [ 0 ] )
2023-03-24 13:10:56 +00:00
}
2023-03-17 16:07:49 +00:00
} else {
2023-05-20 23:00:26 +00:00
msg += t . I18nBot ( "tgbot.commands.usage" )
2023-03-17 16:07:49 +00:00
}
2023-03-24 13:44:26 +00:00
case "inbound" :
2023-05-30 22:01:00 +00:00
onlyMessage = true
2023-05-14 15:20:01 +00:00
if isAdmin && len ( commandArgs ) > 0 {
t . searchInbound ( chatId , commandArgs [ 0 ] )
2023-03-24 13:44:26 +00:00
} else {
2024-10-16 12:39:25 +00:00
handleUnknownCommand ( )
}
case "restart" :
onlyMessage = true
if isAdmin {
if len ( commandArgs ) == 0 {
if t . xrayService . IsXrayRunning ( ) {
err := t . xrayService . RestartXray ( true )
if err != nil {
msg += t . I18nBot ( "tgbot.commands.restartFailed" , "Error==" + err . Error ( ) )
} else {
msg += t . I18nBot ( "tgbot.commands.restartSuccess" )
}
} else {
msg += t . I18nBot ( "tgbot.commands.xrayNotRunning" )
}
} else {
handleUnknownCommand ( )
msg += t . I18nBot ( "tgbot.commands.restartUsage" )
}
} else {
handleUnknownCommand ( )
2023-03-24 13:44:26 +00:00
}
2023-03-17 16:07:49 +00:00
default :
2024-10-16 12:39:25 +00:00
handleUnknownCommand ( )
2023-03-17 16:07:49 +00:00
}
2023-05-30 22:01:00 +00:00
2024-01-29 20:06:03 +00:00
if msg != "" {
2024-10-16 12:39:25 +00:00
t . sendResponse ( chatId , msg , onlyMessage , isAdmin )
}
}
2025-09-20 07:35:50 +00:00
// sendResponse sends the response message based on the onlyMessage flag.
2024-10-16 12:39:25 +00:00
func ( t * Tgbot ) sendResponse ( chatId int64 , msg string , onlyMessage , isAdmin bool ) {
if onlyMessage {
t . SendMsgToTgbot ( chatId , msg )
} else {
t . SendAnswer ( chatId , msg , isAdmin )
2023-05-30 22:01:00 +00:00
}
2023-03-17 16:07:49 +00:00
}
2025-09-20 07:35:50 +00:00
// randomLowerAndNum generates a random string of lowercase letters and numbers.
2025-03-26 18:16:35 +00:00
func ( t * Tgbot ) randomLowerAndNum ( length int ) string {
charset := "abcdefghijklmnopqrstuvwxyz0123456789"
bytes := make ( [ ] byte , length )
for i := range bytes {
randomIndex , _ := rand . Int ( rand . Reader , big . NewInt ( int64 ( len ( charset ) ) ) )
bytes [ i ] = charset [ randomIndex . Int64 ( ) ]
}
return string ( bytes )
}
2025-09-20 07:35:50 +00:00
// answerCallback processes callback queries from inline keyboards.
2024-07-07 09:55:59 +00:00
func ( t * Tgbot ) answerCallback ( callbackQuery * telego . CallbackQuery , isAdmin bool ) {
2024-02-17 17:45:53 +00:00
chatId := callbackQuery . Message . GetChat ( ) . ID
2025-04-06 22:45:52 +00:00
2023-05-04 21:46:43 +00:00
if isAdmin {
2023-05-20 17:16:42 +00:00
// get query from hash storage
2023-05-21 04:03:08 +00:00
decodedQuery , err := t . decodeQuery ( callbackQuery . Data )
2023-05-20 17:16:42 +00:00
if err != nil {
2023-05-21 01:03:01 +00:00
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.noQuery" ) )
2023-05-20 17:16:42 +00:00
return
}
2023-05-20 15:59:28 +00:00
dataArray := strings . Split ( decodedQuery , " " )
2023-05-04 21:46:43 +00:00
if len ( dataArray ) >= 2 && len ( dataArray [ 1 ] ) > 0 {
email := dataArray [ 1 ]
switch dataArray [ 0 ] {
2025-09-16 11:41:48 +00:00
case "get_clients_for_sub" :
inboundId := dataArray [ 1 ]
inboundIdInt , err := strconv . Atoi ( inboundId )
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
clientsKB , err := t . getInboundClientsFor ( inboundIdInt , "client_sub_links" )
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
inbound , _ := t . inboundService . GetInbound ( inboundIdInt )
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.chooseClient" , "Inbound==" + inbound . Remark ) , clientsKB )
case "get_clients_for_individual" :
inboundId := dataArray [ 1 ]
inboundIdInt , err := strconv . Atoi ( inboundId )
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
clientsKB , err := t . getInboundClientsFor ( inboundIdInt , "client_individual_links" )
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
inbound , _ := t . inboundService . GetInbound ( inboundIdInt )
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.chooseClient" , "Inbound==" + inbound . Remark ) , clientsKB )
case "get_clients_for_qr" :
inboundId := dataArray [ 1 ]
inboundIdInt , err := strconv . Atoi ( inboundId )
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
clientsKB , err := t . getInboundClientsFor ( inboundIdInt , "client_qr_links" )
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
inbound , _ := t . inboundService . GetInbound ( inboundIdInt )
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.chooseClient" , "Inbound==" + inbound . Remark ) , clientsKB )
case "client_sub_links" :
t . sendClientSubLinks ( chatId , email )
return
case "client_individual_links" :
t . sendClientIndividualLinks ( chatId , email )
return
case "client_qr_links" :
t . sendClientQRLinks ( chatId , email )
return
2024-01-01 15:07:56 +00:00
case "client_get_usage" :
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.messages.email" , "Email==" + email ) )
t . searchClient ( chatId , email )
2023-05-05 14:50:56 +00:00
case "client_refresh" :
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.clientRefreshSuccess" , "Email==" + email ) )
2024-02-17 17:45:53 +00:00
t . searchClient ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2023-05-05 14:50:56 +00:00
case "client_cancel" :
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.canceled" , "Email==" + email ) )
2024-02-17 17:45:53 +00:00
t . searchClient ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2023-05-05 14:50:56 +00:00
case "ips_refresh" :
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.IpRefreshSuccess" , "Email==" + email ) )
2024-02-17 17:45:53 +00:00
t . searchClientIps ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2023-05-05 14:50:56 +00:00
case "ips_cancel" :
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.canceled" , "Email==" + email ) )
2024-02-17 17:45:53 +00:00
t . searchClientIps ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2023-05-14 19:13:23 +00:00
case "tgid_refresh" :
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.TGIdRefreshSuccess" , "Email==" + email ) )
2024-02-17 17:45:53 +00:00
t . clientTelegramUserInfo ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2023-05-14 19:13:23 +00:00
case "tgid_cancel" :
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.canceled" , "Email==" + email ) )
2024-02-17 17:45:53 +00:00
t . clientTelegramUserInfo ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2023-05-04 21:46:43 +00:00
case "reset_traffic" :
2023-05-14 15:20:01 +00:00
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancelReset" ) ) . WithCallbackData ( t . encodeQuery ( "client_cancel " + email ) ) ,
2023-05-04 21:46:43 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.confirmResetTraffic" ) ) . WithCallbackData ( t . encodeQuery ( "reset_traffic_c " + email ) ) ,
2023-05-04 21:46:43 +00:00
) ,
)
2024-02-17 17:45:53 +00:00
t . editMessageCallbackTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) , inlineKeyboard )
2023-05-05 12:32:16 +00:00
case "reset_traffic_c" :
2023-05-05 01:04:39 +00:00
err := t . inboundService . ResetClientTrafficByEmail ( email )
if err == nil {
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.resetTrafficSuccess" , "Email==" + email ) )
2024-02-17 17:45:53 +00:00
t . searchClient ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2023-05-04 23:17:26 +00:00
} else {
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
2023-05-04 23:17:26 +00:00
}
2023-11-20 14:17:59 +00:00
case "limit_traffic" :
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancel" ) ) . WithCallbackData ( t . encodeQuery ( "client_cancel " + email ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.unlimited" ) ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_c " + email + " 0" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.custom" ) ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_in " + email + " 0" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "1 GB" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_c " + email + " 1" ) ) ,
tu . InlineKeyboardButton ( "5 GB" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_c " + email + " 5" ) ) ,
tu . InlineKeyboardButton ( "10 GB" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_c " + email + " 10" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "20 GB" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_c " + email + " 20" ) ) ,
tu . InlineKeyboardButton ( "30 GB" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_c " + email + " 30" ) ) ,
tu . InlineKeyboardButton ( "40 GB" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_c " + email + " 40" ) ) ,
) ,
tu . InlineKeyboardRow (
2024-01-29 20:06:03 +00:00
tu . InlineKeyboardButton ( "50 GB" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_c " + email + " 50" ) ) ,
2023-11-20 14:17:59 +00:00
tu . InlineKeyboardButton ( "60 GB" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_c " + email + " 60" ) ) ,
tu . InlineKeyboardButton ( "80 GB" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_c " + email + " 80" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "100 GB" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_c " + email + " 100" ) ) ,
tu . InlineKeyboardButton ( "150 GB" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_c " + email + " 150" ) ) ,
tu . InlineKeyboardButton ( "200 GB" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_c " + email + " 200" ) ) ,
) ,
)
2024-02-17 17:45:53 +00:00
t . editMessageCallbackTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) , inlineKeyboard )
2023-11-20 14:17:59 +00:00
case "limit_traffic_c" :
if len ( dataArray ) == 3 {
limitTraffic , err := strconv . Atoi ( dataArray [ 2 ] )
if err == nil {
refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 08:48:28 +00:00
needRestart , err := t . clientService . ResetClientTrafficLimitByEmail ( & t . inboundService , email , limitTraffic )
2024-03-15 18:13:20 +00:00
if needRestart {
2023-11-20 14:17:59 +00:00
t . xrayService . SetToNeedRestart ( )
2024-03-15 18:13:20 +00:00
}
if err == nil {
2023-11-20 14:17:59 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.setTrafficLimitSuccess" , "Email==" + email ) )
2024-02-17 17:45:53 +00:00
t . searchClient ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2023-11-20 14:17:59 +00:00
return
}
}
}
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
2024-02-17 17:45:53 +00:00
t . searchClient ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2023-11-20 14:17:59 +00:00
case "limit_traffic_in" :
if len ( dataArray ) >= 3 {
oldInputNumber , err := strconv . Atoi ( dataArray [ 2 ] )
inputNumber := oldInputNumber
if err == nil {
if len ( dataArray ) == 4 {
num , err := strconv . Atoi ( dataArray [ 3 ] )
if err == nil {
2025-08-17 11:37:49 +00:00
switch num {
case - 2 :
2023-11-20 14:17:59 +00:00
inputNumber = 0
2025-08-17 11:37:49 +00:00
case - 1 :
2023-11-20 14:17:59 +00:00
if inputNumber > 0 {
2024-01-01 15:07:56 +00:00
inputNumber = ( inputNumber / 10 )
2023-11-20 14:17:59 +00:00
}
2025-08-17 11:37:49 +00:00
default :
2023-11-20 14:17:59 +00:00
inputNumber = ( inputNumber * 10 ) + num
}
}
if inputNumber == oldInputNumber {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.successfulOperation" ) )
return
}
if inputNumber >= 999999 {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
return
}
}
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancel" ) ) . WithCallbackData ( t . encodeQuery ( "client_cancel " + email ) ) ,
) ,
tu . InlineKeyboardRow (
2024-01-01 15:07:56 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.confirmNumberAdd" , "Num==" + strconv . Itoa ( inputNumber ) ) ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_c " + email + " " + strconv . Itoa ( inputNumber ) ) ) ,
2023-11-20 14:17:59 +00:00
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "1" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_in " + email + " " + strconv . Itoa ( inputNumber ) + " 1" ) ) ,
tu . InlineKeyboardButton ( "2" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_in " + email + " " + strconv . Itoa ( inputNumber ) + " 2" ) ) ,
tu . InlineKeyboardButton ( "3" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_in " + email + " " + strconv . Itoa ( inputNumber ) + " 3" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "4" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_in " + email + " " + strconv . Itoa ( inputNumber ) + " 4" ) ) ,
tu . InlineKeyboardButton ( "5" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_in " + email + " " + strconv . Itoa ( inputNumber ) + " 5" ) ) ,
tu . InlineKeyboardButton ( "6" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_in " + email + " " + strconv . Itoa ( inputNumber ) + " 6" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "7" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_in " + email + " " + strconv . Itoa ( inputNumber ) + " 7" ) ) ,
tu . InlineKeyboardButton ( "8" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_in " + email + " " + strconv . Itoa ( inputNumber ) + " 8" ) ) ,
tu . InlineKeyboardButton ( "9" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_in " + email + " " + strconv . Itoa ( inputNumber ) + " 9" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "🔄" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_in " + email + " " + strconv . Itoa ( inputNumber ) + " -2" ) ) ,
tu . InlineKeyboardButton ( "0" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_in " + email + " " + strconv . Itoa ( inputNumber ) + " 0" ) ) ,
tu . InlineKeyboardButton ( "⬅️" ) . WithCallbackData ( t . encodeQuery ( "limit_traffic_in " + email + " " + strconv . Itoa ( inputNumber ) + " -1" ) ) ,
) ,
)
2024-02-17 17:45:53 +00:00
t . editMessageCallbackTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) , inlineKeyboard )
2023-11-20 14:17:59 +00:00
return
}
}
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
2024-02-17 17:45:53 +00:00
t . searchClient ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2025-03-26 18:16:35 +00:00
case "add_client_limit_traffic_c" :
2025-12-03 13:58:54 +00:00
limitTraffic , _ := strconv . ParseInt ( dataArray [ 1 ] , 10 , 64 )
client_TotalGB = limitTraffic * 1024 * 1024 * 1024
2025-03-26 18:16:35 +00:00
messageId := callbackQuery . Message . GetMessageID ( )
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>
2026-05-18 00:11:30 +00:00
message_text := t . BuildClientDraftMessage ( )
2025-03-26 18:16:35 +00:00
2025-08-08 18:41:06 +00:00
t . addClient ( callbackQuery . Message . GetChat ( ) . ID , message_text , messageId )
2025-03-26 18:16:35 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.successfulOperation" ) )
case "add_client_limit_traffic_in" :
if len ( dataArray ) >= 2 {
oldInputNumber , err := strconv . Atoi ( dataArray [ 1 ] )
inputNumber := oldInputNumber
if err == nil {
if len ( dataArray ) == 3 {
num , err := strconv . Atoi ( dataArray [ 2 ] )
if err == nil {
2025-08-17 11:37:49 +00:00
switch num {
case - 2 :
2025-03-26 18:16:35 +00:00
inputNumber = 0
2025-08-17 11:37:49 +00:00
case - 1 :
2025-03-26 18:16:35 +00:00
if inputNumber > 0 {
inputNumber = ( inputNumber / 10 )
}
2025-08-17 11:37:49 +00:00
default :
2025-03-26 18:16:35 +00:00
inputNumber = ( inputNumber * 10 ) + num
}
}
if inputNumber == oldInputNumber {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.successfulOperation" ) )
return
}
if inputNumber >= 999999 {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
return
}
}
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancel" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_default_traffic_exp" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.confirmNumberAdd" , "Num==" + strconv . Itoa ( inputNumber ) ) ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_c " + strconv . Itoa ( inputNumber ) ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "1" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_in " + strconv . Itoa ( inputNumber ) + " 1" ) ) ,
tu . InlineKeyboardButton ( "2" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_in " + strconv . Itoa ( inputNumber ) + " 2" ) ) ,
tu . InlineKeyboardButton ( "3" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_in " + strconv . Itoa ( inputNumber ) + " 3" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "4" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_in " + strconv . Itoa ( inputNumber ) + " 4" ) ) ,
tu . InlineKeyboardButton ( "5" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_in " + strconv . Itoa ( inputNumber ) + " 5" ) ) ,
tu . InlineKeyboardButton ( "6" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_in " + strconv . Itoa ( inputNumber ) + " 6" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "7" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_in " + strconv . Itoa ( inputNumber ) + " 7" ) ) ,
tu . InlineKeyboardButton ( "8" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_in " + strconv . Itoa ( inputNumber ) + " 8" ) ) ,
tu . InlineKeyboardButton ( "9" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_in " + strconv . Itoa ( inputNumber ) + " 9" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "🔄" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_in " + strconv . Itoa ( inputNumber ) + " -2" ) ) ,
tu . InlineKeyboardButton ( "0" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_in " + strconv . Itoa ( inputNumber ) + " 0" ) ) ,
tu . InlineKeyboardButton ( "⬅️" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_in " + strconv . Itoa ( inputNumber ) + " -1" ) ) ,
) ,
)
t . editMessageCallbackTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) , inlineKeyboard )
return
}
}
2023-05-05 12:32:16 +00:00
case "reset_exp" :
2023-05-20 23:00:26 +00:00
inlineKeyboard := tu . InlineKeyboard (
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancelReset" ) ) . WithCallbackData ( t . encodeQuery ( "client_cancel " + email ) ) ,
2023-05-04 21:46:43 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.unlimited" ) ) . WithCallbackData ( t . encodeQuery ( "reset_exp_c " + email + " 0" ) ) ,
2023-11-20 14:17:59 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.custom" ) ) . WithCallbackData ( t . encodeQuery ( "reset_exp_in " + email + " 0" ) ) ,
2023-05-04 22:18:37 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2024-01-01 15:07:56 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.add" ) + " 7 " + t . I18nBot ( "tgbot.days" ) ) . WithCallbackData ( t . encodeQuery ( "reset_exp_c " + email + " 7" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.add" ) + " 10 " + t . I18nBot ( "tgbot.days" ) ) . WithCallbackData ( t . encodeQuery ( "reset_exp_c " + email + " 10" ) ) ,
2023-05-04 21:46:43 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2024-01-01 15:07:56 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.add" ) + " 14 " + t . I18nBot ( "tgbot.days" ) ) . WithCallbackData ( t . encodeQuery ( "reset_exp_c " + email + " 14" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.add" ) + " 20 " + t . I18nBot ( "tgbot.days" ) ) . WithCallbackData ( t . encodeQuery ( "reset_exp_c " + email + " 20" ) ) ,
2023-05-04 21:46:43 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2024-01-01 15:07:56 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.add" ) + " 1 " + t . I18nBot ( "tgbot.month" ) ) . WithCallbackData ( t . encodeQuery ( "reset_exp_c " + email + " 30" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.add" ) + " 3 " + t . I18nBot ( "tgbot.months" ) ) . WithCallbackData ( t . encodeQuery ( "reset_exp_c " + email + " 90" ) ) ,
2023-05-04 21:46:43 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2024-01-01 15:07:56 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.add" ) + " 6 " + t . I18nBot ( "tgbot.months" ) ) . WithCallbackData ( t . encodeQuery ( "reset_exp_c " + email + " 180" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.add" ) + " 12 " + t . I18nBot ( "tgbot.months" ) ) . WithCallbackData ( t . encodeQuery ( "reset_exp_c " + email + " 365" ) ) ,
2023-05-04 21:46:43 +00:00
) ,
)
2024-02-17 17:45:53 +00:00
t . editMessageCallbackTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) , inlineKeyboard )
2023-05-05 12:32:16 +00:00
case "reset_exp_c" :
2023-05-05 01:04:39 +00:00
if len ( dataArray ) == 3 {
2025-12-03 13:58:54 +00:00
days , err := strconv . ParseInt ( dataArray [ 2 ] , 10 , 64 )
2023-05-05 01:04:39 +00:00
if err == nil {
2025-09-19 08:46:49 +00:00
var date int64
2023-05-04 22:18:37 +00:00
if days > 0 {
2024-01-01 15:07:56 +00:00
traffic , err := t . inboundService . GetClientTrafficByEmail ( email )
if err != nil {
logger . Warning ( err )
msg := t . I18nBot ( "tgbot.wentWrong" )
t . SendMsgToTgbot ( chatId , msg )
return
}
if traffic == nil {
msg := t . I18nBot ( "tgbot.noResult" )
t . SendMsgToTgbot ( chatId , msg )
return
}
if traffic . ExpiryTime > 0 {
if traffic . ExpiryTime - time . Now ( ) . Unix ( ) * 1000 < 0 {
date = - int64 ( days * 24 * 60 * 60000 )
} else {
date = traffic . ExpiryTime + int64 ( days * 24 * 60 * 60000 )
}
} else {
date = traffic . ExpiryTime - int64 ( days * 24 * 60 * 60000 )
}
2023-05-04 22:18:37 +00:00
}
refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 08:48:28 +00:00
needRestart , err := t . clientService . ResetClientExpiryTimeByEmail ( & t . inboundService , email , date )
2024-03-15 18:13:20 +00:00
if needRestart {
2023-05-04 23:17:26 +00:00
t . xrayService . SetToNeedRestart ( )
2024-03-15 18:13:20 +00:00
}
if err == nil {
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.expireResetSuccess" , "Email==" + email ) )
2024-02-17 17:45:53 +00:00
t . searchClient ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2023-05-05 01:04:39 +00:00
return
2023-05-04 23:17:26 +00:00
}
2023-05-04 21:46:43 +00:00
}
}
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
2024-02-17 17:45:53 +00:00
t . searchClient ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2023-11-20 14:17:59 +00:00
case "reset_exp_in" :
if len ( dataArray ) >= 3 {
oldInputNumber , err := strconv . Atoi ( dataArray [ 2 ] )
inputNumber := oldInputNumber
if err == nil {
if len ( dataArray ) == 4 {
num , err := strconv . Atoi ( dataArray [ 3 ] )
if err == nil {
2025-08-17 11:37:49 +00:00
switch num {
case - 2 :
2023-11-20 14:17:59 +00:00
inputNumber = 0
2025-08-17 11:37:49 +00:00
case - 1 :
2023-11-20 14:17:59 +00:00
if inputNumber > 0 {
2024-01-01 15:07:56 +00:00
inputNumber = ( inputNumber / 10 )
2023-11-20 14:17:59 +00:00
}
2025-08-17 11:37:49 +00:00
default :
2023-11-20 14:17:59 +00:00
inputNumber = ( inputNumber * 10 ) + num
}
}
if inputNumber == oldInputNumber {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.successfulOperation" ) )
return
}
if inputNumber >= 999999 {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
return
}
}
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancel" ) ) . WithCallbackData ( t . encodeQuery ( "client_cancel " + email ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.confirmNumber" , "Num==" + strconv . Itoa ( inputNumber ) ) ) . WithCallbackData ( t . encodeQuery ( "reset_exp_c " + email + " " + strconv . Itoa ( inputNumber ) ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "1" ) . WithCallbackData ( t . encodeQuery ( "reset_exp_in " + email + " " + strconv . Itoa ( inputNumber ) + " 1" ) ) ,
tu . InlineKeyboardButton ( "2" ) . WithCallbackData ( t . encodeQuery ( "reset_exp_in " + email + " " + strconv . Itoa ( inputNumber ) + " 2" ) ) ,
tu . InlineKeyboardButton ( "3" ) . WithCallbackData ( t . encodeQuery ( "reset_exp_in " + email + " " + strconv . Itoa ( inputNumber ) + " 3" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "4" ) . WithCallbackData ( t . encodeQuery ( "reset_exp_in " + email + " " + strconv . Itoa ( inputNumber ) + " 4" ) ) ,
tu . InlineKeyboardButton ( "5" ) . WithCallbackData ( t . encodeQuery ( "reset_exp_in " + email + " " + strconv . Itoa ( inputNumber ) + " 5" ) ) ,
tu . InlineKeyboardButton ( "6" ) . WithCallbackData ( t . encodeQuery ( "reset_exp_in " + email + " " + strconv . Itoa ( inputNumber ) + " 6" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "7" ) . WithCallbackData ( t . encodeQuery ( "reset_exp_in " + email + " " + strconv . Itoa ( inputNumber ) + " 7" ) ) ,
tu . InlineKeyboardButton ( "8" ) . WithCallbackData ( t . encodeQuery ( "reset_exp_in " + email + " " + strconv . Itoa ( inputNumber ) + " 8" ) ) ,
tu . InlineKeyboardButton ( "9" ) . WithCallbackData ( t . encodeQuery ( "reset_exp_in " + email + " " + strconv . Itoa ( inputNumber ) + " 9" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "🔄" ) . WithCallbackData ( t . encodeQuery ( "reset_exp_in " + email + " " + strconv . Itoa ( inputNumber ) + " -2" ) ) ,
tu . InlineKeyboardButton ( "0" ) . WithCallbackData ( t . encodeQuery ( "reset_exp_in " + email + " " + strconv . Itoa ( inputNumber ) + " 0" ) ) ,
tu . InlineKeyboardButton ( "⬅️" ) . WithCallbackData ( t . encodeQuery ( "reset_exp_in " + email + " " + strconv . Itoa ( inputNumber ) + " -1" ) ) ,
) ,
)
2024-02-17 17:45:53 +00:00
t . editMessageCallbackTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) , inlineKeyboard )
2023-11-20 14:17:59 +00:00
return
}
}
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
2024-02-17 17:45:53 +00:00
t . searchClient ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2025-03-26 18:16:35 +00:00
case "add_client_reset_exp_c" :
client_ExpiryTime = 0
2025-12-03 13:58:54 +00:00
days , _ := strconv . ParseInt ( dataArray [ 1 ] , 10 , 64 )
2025-09-19 08:46:49 +00:00
var date int64
2025-03-26 18:16:35 +00:00
if client_ExpiryTime > 0 {
if client_ExpiryTime - time . Now ( ) . Unix ( ) * 1000 < 0 {
date = - int64 ( days * 24 * 60 * 60000 )
} else {
date = client_ExpiryTime + int64 ( days * 24 * 60 * 60000 )
}
} else {
date = client_ExpiryTime - int64 ( days * 24 * 60 * 60000 )
}
client_ExpiryTime = date
2025-04-06 22:45:52 +00:00
2025-03-26 18:16:35 +00:00
messageId := callbackQuery . Message . GetMessageID ( )
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>
2026-05-18 00:11:30 +00:00
message_text := t . BuildClientDraftMessage ( )
2025-03-26 18:16:35 +00:00
2025-08-08 18:41:06 +00:00
t . addClient ( callbackQuery . Message . GetChat ( ) . ID , message_text , messageId )
2025-03-26 18:16:35 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.successfulOperation" ) )
case "add_client_reset_exp_in" :
if len ( dataArray ) >= 2 {
oldInputNumber , err := strconv . Atoi ( dataArray [ 1 ] )
inputNumber := oldInputNumber
if err == nil {
if len ( dataArray ) == 3 {
num , err := strconv . Atoi ( dataArray [ 2 ] )
if err == nil {
2025-08-17 11:37:49 +00:00
switch num {
case - 2 :
2025-03-26 18:16:35 +00:00
inputNumber = 0
2025-08-17 11:37:49 +00:00
case - 1 :
2025-03-26 18:16:35 +00:00
if inputNumber > 0 {
inputNumber = ( inputNumber / 10 )
}
2025-08-17 11:37:49 +00:00
default :
2025-03-26 18:16:35 +00:00
inputNumber = ( inputNumber * 10 ) + num
}
}
if inputNumber == oldInputNumber {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.successfulOperation" ) )
return
}
if inputNumber >= 999999 {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
return
}
}
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancel" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_default_traffic_exp" ) ) ,
) ,
tu . InlineKeyboardRow (
2025-05-28 08:26:29 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.confirmNumberAdd" , "Num==" + strconv . Itoa ( inputNumber ) ) ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_c " + strconv . Itoa ( inputNumber ) ) ) ,
2025-03-26 18:16:35 +00:00
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "1" ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_in " + strconv . Itoa ( inputNumber ) + " 1" ) ) ,
tu . InlineKeyboardButton ( "2" ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_in " + strconv . Itoa ( inputNumber ) + " 2" ) ) ,
tu . InlineKeyboardButton ( "3" ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_in " + strconv . Itoa ( inputNumber ) + " 3" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "4" ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_in " + strconv . Itoa ( inputNumber ) + " 4" ) ) ,
tu . InlineKeyboardButton ( "5" ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_in " + strconv . Itoa ( inputNumber ) + " 5" ) ) ,
tu . InlineKeyboardButton ( "6" ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_in " + strconv . Itoa ( inputNumber ) + " 6" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "7" ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_in " + strconv . Itoa ( inputNumber ) + " 7" ) ) ,
tu . InlineKeyboardButton ( "8" ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_in " + strconv . Itoa ( inputNumber ) + " 8" ) ) ,
tu . InlineKeyboardButton ( "9" ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_in " + strconv . Itoa ( inputNumber ) + " 9" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "🔄" ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_in " + strconv . Itoa ( inputNumber ) + " -2" ) ) ,
tu . InlineKeyboardButton ( "0" ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_in " + strconv . Itoa ( inputNumber ) + " 0" ) ) ,
tu . InlineKeyboardButton ( "⬅️" ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_in " + strconv . Itoa ( inputNumber ) + " -1" ) ) ,
) ,
)
t . editMessageCallbackTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) , inlineKeyboard )
return
}
}
2023-05-05 14:50:56 +00:00
case "ip_limit" :
2023-05-14 15:20:01 +00:00
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancelIpLimit" ) ) . WithCallbackData ( t . encodeQuery ( "client_cancel " + email ) ) ,
2023-05-05 14:50:56 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.unlimited" ) ) . WithCallbackData ( t . encodeQuery ( "ip_limit_c " + email + " 0" ) ) ,
2023-11-20 14:17:59 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.custom" ) ) . WithCallbackData ( t . encodeQuery ( "ip_limit_in " + email + " 0" ) ) ,
2023-05-05 14:50:56 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( "1" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_c " + email + " 1" ) ) ,
tu . InlineKeyboardButton ( "2" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_c " + email + " 2" ) ) ,
2023-05-05 14:50:56 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( "3" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_c " + email + " 3" ) ) ,
tu . InlineKeyboardButton ( "4" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_c " + email + " 4" ) ) ,
2023-05-05 14:50:56 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( "5" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_c " + email + " 5" ) ) ,
tu . InlineKeyboardButton ( "6" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_c " + email + " 6" ) ) ,
tu . InlineKeyboardButton ( "7" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_c " + email + " 7" ) ) ,
2023-05-05 14:50:56 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( "8" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_c " + email + " 8" ) ) ,
tu . InlineKeyboardButton ( "9" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_c " + email + " 9" ) ) ,
tu . InlineKeyboardButton ( "10" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_c " + email + " 10" ) ) ,
2023-05-05 14:50:56 +00:00
) ,
)
2024-02-17 17:45:53 +00:00
t . editMessageCallbackTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) , inlineKeyboard )
2023-05-05 14:50:56 +00:00
case "ip_limit_c" :
if len ( dataArray ) == 3 {
count , err := strconv . Atoi ( dataArray [ 2 ] )
if err == nil {
refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 08:48:28 +00:00
needRestart , err := t . clientService . ResetClientIpLimitByEmail ( & t . inboundService , email , count )
2024-03-15 18:13:20 +00:00
if needRestart {
2023-05-05 14:50:56 +00:00
t . xrayService . SetToNeedRestart ( )
2024-03-15 18:13:20 +00:00
}
if err == nil {
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.resetIpSuccess" , "Email==" + email , "Count==" + strconv . Itoa ( count ) ) )
2024-02-17 17:45:53 +00:00
t . searchClient ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2023-05-05 14:50:56 +00:00
return
}
}
}
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
2024-02-17 17:45:53 +00:00
t . searchClient ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2023-11-20 14:17:59 +00:00
case "ip_limit_in" :
if len ( dataArray ) >= 3 {
oldInputNumber , err := strconv . Atoi ( dataArray [ 2 ] )
inputNumber := oldInputNumber
if err == nil {
if len ( dataArray ) == 4 {
num , err := strconv . Atoi ( dataArray [ 3 ] )
if err == nil {
2025-08-17 11:37:49 +00:00
switch num {
case - 2 :
2023-11-20 14:17:59 +00:00
inputNumber = 0
2025-08-17 11:37:49 +00:00
case - 1 :
2023-11-20 14:17:59 +00:00
if inputNumber > 0 {
2024-01-01 15:07:56 +00:00
inputNumber = ( inputNumber / 10 )
2023-11-20 14:17:59 +00:00
}
2025-08-17 11:37:49 +00:00
default :
2023-11-20 14:17:59 +00:00
inputNumber = ( inputNumber * 10 ) + num
}
}
if inputNumber == oldInputNumber {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.successfulOperation" ) )
return
}
if inputNumber >= 999999 {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
return
}
}
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancel" ) ) . WithCallbackData ( t . encodeQuery ( "client_cancel " + email ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.confirmNumber" , "Num==" + strconv . Itoa ( inputNumber ) ) ) . WithCallbackData ( t . encodeQuery ( "ip_limit_c " + email + " " + strconv . Itoa ( inputNumber ) ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "1" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_in " + email + " " + strconv . Itoa ( inputNumber ) + " 1" ) ) ,
tu . InlineKeyboardButton ( "2" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_in " + email + " " + strconv . Itoa ( inputNumber ) + " 2" ) ) ,
tu . InlineKeyboardButton ( "3" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_in " + email + " " + strconv . Itoa ( inputNumber ) + " 3" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "4" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_in " + email + " " + strconv . Itoa ( inputNumber ) + " 4" ) ) ,
tu . InlineKeyboardButton ( "5" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_in " + email + " " + strconv . Itoa ( inputNumber ) + " 5" ) ) ,
tu . InlineKeyboardButton ( "6" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_in " + email + " " + strconv . Itoa ( inputNumber ) + " 6" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "7" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_in " + email + " " + strconv . Itoa ( inputNumber ) + " 7" ) ) ,
tu . InlineKeyboardButton ( "8" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_in " + email + " " + strconv . Itoa ( inputNumber ) + " 8" ) ) ,
tu . InlineKeyboardButton ( "9" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_in " + email + " " + strconv . Itoa ( inputNumber ) + " 9" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "🔄" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_in " + email + " " + strconv . Itoa ( inputNumber ) + " -2" ) ) ,
tu . InlineKeyboardButton ( "0" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_in " + email + " " + strconv . Itoa ( inputNumber ) + " 0" ) ) ,
tu . InlineKeyboardButton ( "⬅️" ) . WithCallbackData ( t . encodeQuery ( "ip_limit_in " + email + " " + strconv . Itoa ( inputNumber ) + " -1" ) ) ,
) ,
)
2024-02-17 17:45:53 +00:00
t . editMessageCallbackTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) , inlineKeyboard )
2023-11-20 14:17:59 +00:00
return
}
}
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
2024-02-17 17:45:53 +00:00
t . searchClient ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2025-05-06 16:27:17 +00:00
case "add_client_ip_limit_c" :
if len ( dataArray ) == 2 {
count , _ := strconv . Atoi ( dataArray [ 1 ] )
client_LimitIP = count
}
messageId := callbackQuery . Message . GetMessageID ( )
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>
2026-05-18 00:11:30 +00:00
message_text := t . BuildClientDraftMessage ( )
2025-05-06 16:27:17 +00:00
2025-08-08 18:41:06 +00:00
t . addClient ( callbackQuery . Message . GetChat ( ) . ID , message_text , messageId )
2025-05-06 16:27:17 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.successfulOperation" ) )
case "add_client_ip_limit_in" :
if len ( dataArray ) >= 2 {
oldInputNumber , err := strconv . Atoi ( dataArray [ 1 ] )
inputNumber := oldInputNumber
if err == nil {
if len ( dataArray ) == 3 {
num , err := strconv . Atoi ( dataArray [ 2 ] )
if err == nil {
2025-08-17 11:37:49 +00:00
switch num {
case - 2 :
2025-05-06 16:27:17 +00:00
inputNumber = 0
2025-08-17 11:37:49 +00:00
case - 1 :
2025-05-06 16:27:17 +00:00
if inputNumber > 0 {
inputNumber = ( inputNumber / 10 )
}
2025-08-17 11:37:49 +00:00
default :
2025-05-06 16:27:17 +00:00
inputNumber = ( inputNumber * 10 ) + num
}
}
if inputNumber == oldInputNumber {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.successfulOperation" ) )
return
}
if inputNumber >= 999999 {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
return
}
}
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancel" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_default_ip_limit" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.confirmNumber" , "Num==" + strconv . Itoa ( inputNumber ) ) ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_c " + strconv . Itoa ( inputNumber ) ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "1" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_in " + strconv . Itoa ( inputNumber ) + " 1" ) ) ,
tu . InlineKeyboardButton ( "2" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_in " + strconv . Itoa ( inputNumber ) + " 2" ) ) ,
tu . InlineKeyboardButton ( "3" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_in " + strconv . Itoa ( inputNumber ) + " 3" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "4" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_in " + strconv . Itoa ( inputNumber ) + " 4" ) ) ,
tu . InlineKeyboardButton ( "5" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_in " + strconv . Itoa ( inputNumber ) + " 5" ) ) ,
tu . InlineKeyboardButton ( "6" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_in " + strconv . Itoa ( inputNumber ) + " 6" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "7" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_in " + strconv . Itoa ( inputNumber ) + " 7" ) ) ,
tu . InlineKeyboardButton ( "8" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_in " + strconv . Itoa ( inputNumber ) + " 8" ) ) ,
tu . InlineKeyboardButton ( "9" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_in " + strconv . Itoa ( inputNumber ) + " 9" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "🔄" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_in " + strconv . Itoa ( inputNumber ) + " -2" ) ) ,
tu . InlineKeyboardButton ( "0" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_in " + strconv . Itoa ( inputNumber ) + " 0" ) ) ,
tu . InlineKeyboardButton ( "⬅️" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_in " + strconv . Itoa ( inputNumber ) + " -1" ) ) ,
) ,
)
t . editMessageCallbackTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) , inlineKeyboard )
return
}
}
2023-05-05 14:50:56 +00:00
case "clear_ips" :
2023-05-14 15:20:01 +00:00
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancel" ) ) . WithCallbackData ( t . encodeQuery ( "ips_cancel " + email ) ) ,
2023-05-05 14:50:56 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.confirmClearIps" ) ) . WithCallbackData ( t . encodeQuery ( "clear_ips_c " + email ) ) ,
2023-05-05 14:50:56 +00:00
) ,
)
2024-02-17 17:45:53 +00:00
t . editMessageCallbackTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) , inlineKeyboard )
2023-05-05 14:50:56 +00:00
case "clear_ips_c" :
err := t . inboundService . ClearClientIps ( email )
if err == nil {
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.clearIpSuccess" , "Email==" + email ) )
2024-02-17 17:45:53 +00:00
t . searchClientIps ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2023-05-05 14:50:56 +00:00
} else {
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
2023-05-05 14:50:56 +00:00
}
case "ip_log" :
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.getIpLog" , "Email==" + email ) )
2023-05-14 15:20:01 +00:00
t . searchClientIps ( chatId , email )
2023-05-14 18:37:49 +00:00
case "tg_user" :
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.getUserInfo" , "Email==" + email ) )
2023-05-14 18:37:49 +00:00
t . clientTelegramUserInfo ( chatId , email )
2023-05-14 19:13:23 +00:00
case "tgid_remove" :
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancel" ) ) . WithCallbackData ( t . encodeQuery ( "tgid_cancel " + email ) ) ,
2023-05-14 19:13:23 +00:00
) ,
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.confirmRemoveTGUser" ) ) . WithCallbackData ( t . encodeQuery ( "tgid_remove_c " + email ) ) ,
2023-05-14 19:13:23 +00:00
) ,
)
2024-02-17 17:45:53 +00:00
t . editMessageCallbackTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) , inlineKeyboard )
2023-05-14 19:13:23 +00:00
case "tgid_remove_c" :
traffic , err := t . inboundService . GetClientTrafficByEmail ( email )
if err != nil || traffic == nil {
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
2023-05-14 19:13:23 +00:00
return
}
refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 08:48:28 +00:00
needRestart , err := t . clientService . SetClientTelegramUserID ( & t . inboundService , traffic . Id , EmptyTelegramUserID )
2024-03-15 18:13:20 +00:00
if needRestart {
t . xrayService . SetToNeedRestart ( )
}
2023-05-14 19:13:23 +00:00
if err == nil {
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.removedTGUserSuccess" , "Email==" + email ) )
2024-02-17 17:45:53 +00:00
t . clientTelegramUserInfo ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2023-05-14 19:13:23 +00:00
} else {
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
2023-05-14 19:13:23 +00:00
}
2023-05-05 14:50:56 +00:00
case "toggle_enable" :
2024-01-01 15:07:56 +00:00
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancel" ) ) . WithCallbackData ( t . encodeQuery ( "client_cancel " + email ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.confirmToggle" ) ) . WithCallbackData ( t . encodeQuery ( "toggle_enable_c " + email ) ) ,
) ,
)
2024-02-17 17:45:53 +00:00
t . editMessageCallbackTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) , inlineKeyboard )
2024-01-01 15:07:56 +00:00
case "toggle_enable_c" :
refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 08:48:28 +00:00
enabled , needRestart , err := t . clientService . ToggleClientEnableByEmail ( & t . inboundService , email )
2024-03-15 18:13:20 +00:00
if needRestart {
2023-05-05 14:50:56 +00:00
t . xrayService . SetToNeedRestart ( )
2024-03-15 18:13:20 +00:00
}
if err == nil {
2023-05-05 16:20:40 +00:00
if enabled {
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.enableSuccess" , "Email==" + email ) )
2023-05-05 14:50:56 +00:00
} else {
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.disableSuccess" , "Email==" + email ) )
2023-05-05 14:50:56 +00:00
}
2024-02-17 17:45:53 +00:00
t . searchClient ( chatId , email , callbackQuery . Message . GetMessageID ( ) )
2023-05-05 14:50:56 +00:00
} else {
2023-05-20 23:00:26 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.errorOperation" ) )
2023-05-05 14:50:56 +00:00
}
2024-08-18 21:30:56 +00:00
case "get_clients" :
inboundId := dataArray [ 1 ]
inboundIdInt , err := strconv . Atoi ( inboundId )
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
inbound , err := t . inboundService . GetInbound ( inboundIdInt )
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
clients , err := t . getInboundClients ( inboundIdInt )
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.chooseClient" , "Inbound==" + inbound . Remark ) , clients )
2025-03-26 18:16:35 +00:00
case "add_client_to" :
2025-04-06 22:45:52 +00:00
client_Email = t . randomLowerAndNum ( 8 )
2025-03-26 18:16:35 +00:00
client_LimitIP = 0
client_TotalGB = 0
client_ExpiryTime = 0
2025-04-06 22:45:52 +00:00
client_Enable = true
2025-03-26 18:16:35 +00:00
client_TgID = ""
client_SubID = t . randomLowerAndNum ( 16 )
2025-04-06 22:45:52 +00:00
client_Comment = ""
client_Reset = 0
2024-08-18 21:30:56 +00:00
2025-03-26 18:16:35 +00:00
inboundId := dataArray [ 1 ]
inboundIdInt , err := strconv . Atoi ( inboundId )
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
receiver_inbound_ID = inboundIdInt
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>
2026-05-18 00:11:30 +00:00
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 )
2025-03-26 18:16:35 +00:00
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
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>
2026-05-18 00:11:30 +00:00
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 ( )
2025-08-17 11:37:49 +00:00
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
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>
2026-05-18 00:11:30 +00:00
t . editMessageCallbackTgBot ( callbackQuery . Message . GetChat ( ) . ID , callbackQuery . Message . GetMessageID ( ) , picker )
2023-05-04 21:46:43 +00:00
}
return
2024-08-18 21:30:56 +00:00
} else {
switch callbackQuery . Data {
case "get_inbounds" :
inbounds , err := t . getInbounds ( )
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.buttons.allClients" ) )
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.chooseInbound" ) , inbounds )
2025-09-16 11:41:48 +00:00
case "admin_client_sub_links" :
inbounds , err := t . getInboundsFor ( "get_clients_for_sub" )
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.chooseInbound" ) , inbounds )
case "admin_client_individual_links" :
inbounds , err := t . getInboundsFor ( "get_clients_for_individual" )
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.chooseInbound" ) , inbounds )
case "admin_client_qr_links" :
inbounds , err := t . getInboundsFor ( "get_clients_for_qr" )
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.chooseInbound" ) , inbounds )
2024-08-18 21:30:56 +00:00
}
2023-05-04 21:46:43 +00:00
}
}
2023-03-17 16:07:49 +00:00
switch callbackQuery . Data {
case "get_usage" :
2024-01-01 15:07:56 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.buttons.serverUsage" ) )
2024-05-14 12:00:10 +00:00
t . getServerUsage ( chatId )
case "usage_refresh" :
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.successfulOperation" ) )
t . getServerUsage ( chatId , callbackQuery . Message . GetMessageID ( ) )
2023-03-17 16:07:49 +00:00
case "inbounds" :
2024-01-01 15:07:56 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.buttons.getInbounds" ) )
2023-05-14 15:20:01 +00:00
t . SendMsgToTgbot ( chatId , t . getInboundUsages ( ) )
2023-04-09 19:43:18 +00:00
case "deplete_soon" :
2024-01-01 15:07:56 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.buttons.depleteSoon" ) )
t . getExhausted ( chatId )
2023-03-17 16:07:49 +00:00
case "get_backup" :
2024-01-01 15:07:56 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.buttons.dbBackup" ) )
2023-05-14 15:20:01 +00:00
t . sendBackup ( chatId )
2024-01-01 15:07:56 +00:00
case "get_banlogs" :
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.buttons.getBanLogs" ) )
t . sendBanLogs ( chatId , true )
2023-03-17 16:07:49 +00:00
case "client_traffic" :
2024-04-02 11:34:44 +00:00
tgUserID := callbackQuery . From . ID
2024-01-01 15:07:56 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.buttons.clientUsage" ) )
2024-04-02 11:34:44 +00:00
t . getClientUsage ( chatId , tgUserID )
2023-03-17 16:07:49 +00:00
case "client_commands" :
2024-01-01 15:07:56 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.buttons.commands" ) )
2023-05-20 23:00:26 +00:00
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.commands.helpClientCommands" ) )
2025-09-14 17:51:57 +00:00
case "client_sub_links" :
// show user's own clients to choose one for sub links
tgUserID := callbackQuery . From . ID
traffics , err := t . inboundService . GetClientTrafficTgBot ( tgUserID )
if err != nil {
// fallback to message
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.errorOperation" ) + "\r\n" + err . Error ( ) )
return
}
if len ( traffics ) == 0 {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.askToAddUserId" , "TgUserID==" + strconv . FormatInt ( tgUserID , 10 ) ) )
return
}
var buttons [ ] telego . InlineKeyboardButton
for _ , tr := range traffics {
buttons = append ( buttons , tu . InlineKeyboardButton ( tr . Email ) . WithCallbackData ( t . encodeQuery ( "client_sub_links " + tr . Email ) ) )
}
cols := 1
if len ( buttons ) >= 6 {
cols = 2
}
keyboard := tu . InlineKeyboardGrid ( tu . InlineKeyboardCols ( cols , buttons ... ) )
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.commands.pleaseChoose" ) , keyboard )
case "client_individual_links" :
// show user's clients to choose for individual links
tgUserID := callbackQuery . From . ID
traffics , err := t . inboundService . GetClientTrafficTgBot ( tgUserID )
if err != nil {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.errorOperation" ) + "\r\n" + err . Error ( ) )
return
}
if len ( traffics ) == 0 {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.askToAddUserId" , "TgUserID==" + strconv . FormatInt ( tgUserID , 10 ) ) )
return
}
var buttons2 [ ] telego . InlineKeyboardButton
for _ , tr := range traffics {
buttons2 = append ( buttons2 , tu . InlineKeyboardButton ( tr . Email ) . WithCallbackData ( t . encodeQuery ( "client_individual_links " + tr . Email ) ) )
}
cols2 := 1
if len ( buttons2 ) >= 6 {
cols2 = 2
}
keyboard2 := tu . InlineKeyboardGrid ( tu . InlineKeyboardCols ( cols2 , buttons2 ... ) )
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.commands.pleaseChoose" ) , keyboard2 )
case "client_qr_links" :
// show user's clients to choose for QR codes
tgUserID := callbackQuery . From . ID
traffics , err := t . inboundService . GetClientTrafficTgBot ( tgUserID )
if err != nil {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.errorOccurred" ) + "\r\n" + err . Error ( ) )
return
}
if len ( traffics ) == 0 {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.askToAddUserId" , "TgUserID==" + strconv . FormatInt ( tgUserID , 10 ) ) )
return
}
var buttons3 [ ] telego . InlineKeyboardButton
for _ , tr := range traffics {
buttons3 = append ( buttons3 , tu . InlineKeyboardButton ( tr . Email ) . WithCallbackData ( t . encodeQuery ( "client_qr_links " + tr . Email ) ) )
}
cols3 := 1
if len ( buttons3 ) >= 6 {
cols3 = 2
}
keyboard3 := tu . InlineKeyboardGrid ( tu . InlineKeyboardCols ( cols3 , buttons3 ... ) )
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.commands.pleaseChoose" ) , keyboard3 )
2024-01-01 15:07:56 +00:00
case "onlines" :
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.buttons.onlines" ) )
t . onlineClients ( chatId )
case "onlines_refresh" :
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.successfulOperation" ) )
2024-02-17 17:45:53 +00:00
t . onlineClients ( chatId , callbackQuery . Message . GetMessageID ( ) )
2023-03-17 16:07:49 +00:00
case "commands" :
2024-01-01 15:07:56 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.buttons.commands" ) )
2023-05-20 23:00:26 +00:00
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.commands.helpAdminCommands" ) )
2025-03-26 18:16:35 +00:00
case "add_client" :
2025-04-06 22:45:52 +00:00
client_Email = t . randomLowerAndNum ( 8 )
2025-03-26 18:16:35 +00:00
client_LimitIP = 0
client_TotalGB = 0
client_ExpiryTime = 0
2025-04-06 22:45:52 +00:00
client_Enable = true
2025-03-26 18:16:35 +00:00
client_TgID = ""
client_SubID = t . randomLowerAndNum ( 16 )
2025-04-06 22:45:52 +00:00
client_Comment = ""
client_Reset = 0
2025-03-26 18:16:35 +00:00
inbounds , err := t . getInboundsAddClient ( )
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.buttons.addClient" ) )
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.chooseInbound" ) , inbounds )
case "add_client_ch_default_email" :
t . deleteMessageTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) )
userStates [ chatId ] = "awaiting_email"
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.email_prompt" , "ClientEmail==" + client_Email )
t . SendMsgToTgbot ( chatId , prompt_message , cancel_btn_markup )
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>
2026-05-18 00:11:30 +00:00
case "add_client_ch_default_comment" :
2025-03-26 18:16:35 +00:00
t . deleteMessageTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) )
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>
2026-05-18 00:11:30 +00:00
userStates [ chatId ] = "awaiting_comment"
2025-03-26 18:16:35 +00:00
cancel_btn_markup := tu . InlineKeyboard (
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.use_default" ) ) . WithCallbackData ( "add_client_default_info" ) ,
) ,
)
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>
2026-05-18 00:11:30 +00:00
prompt_message := t . I18nBot ( "tgbot.messages.comment_prompt" , "ClientComment==" + client_Comment )
2025-03-26 18:16:35 +00:00
t . SendMsgToTgbot ( chatId , prompt_message , cancel_btn_markup )
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>
2026-05-18 00:11:30 +00:00
case "add_client_ch_default_tg_id" :
2025-03-26 18:16:35 +00:00
t . deleteMessageTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) )
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>
2026-05-18 00:11:30 +00:00
userStates [ chatId ] = "awaiting_tg_id"
2025-03-26 18:16:35 +00:00
cancel_btn_markup := tu . InlineKeyboard (
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.use_default" ) ) . WithCallbackData ( "add_client_default_info" ) ,
) ,
)
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>
2026-05-18 00:11:30 +00:00
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 )
2025-03-26 18:16:35 +00:00
case "add_client_ch_default_traffic" :
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancel" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_default_traffic_exp" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.unlimited" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_c 0" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.custom" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_in 0" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "1 GB" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_c 1" ) ) ,
tu . InlineKeyboardButton ( "5 GB" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_c 5" ) ) ,
tu . InlineKeyboardButton ( "10 GB" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_c 10" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "20 GB" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_c 20" ) ) ,
tu . InlineKeyboardButton ( "30 GB" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_c 30" ) ) ,
tu . InlineKeyboardButton ( "40 GB" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_c 40" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "50 GB" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_c 50" ) ) ,
tu . InlineKeyboardButton ( "60 GB" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_c 60" ) ) ,
tu . InlineKeyboardButton ( "80 GB" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_c 80" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "100 GB" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_c 100" ) ) ,
tu . InlineKeyboardButton ( "150 GB" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_c 150" ) ) ,
tu . InlineKeyboardButton ( "200 GB" ) . WithCallbackData ( t . encodeQuery ( "add_client_limit_traffic_c 200" ) ) ,
) ,
)
t . editMessageCallbackTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) , inlineKeyboard )
case "add_client_ch_default_exp" :
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancel" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_default_traffic_exp" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.unlimited" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_c 0" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.custom" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_in 0" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.add" ) + " 7 " + t . I18nBot ( "tgbot.days" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_c 7" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.add" ) + " 10 " + t . I18nBot ( "tgbot.days" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_c 10" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.add" ) + " 14 " + t . I18nBot ( "tgbot.days" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_c 14" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.add" ) + " 20 " + t . I18nBot ( "tgbot.days" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_c 20" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.add" ) + " 1 " + t . I18nBot ( "tgbot.month" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_c 30" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.add" ) + " 3 " + t . I18nBot ( "tgbot.months" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_c 90" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.add" ) + " 6 " + t . I18nBot ( "tgbot.months" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_c 180" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.add" ) + " 12 " + t . I18nBot ( "tgbot.months" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_reset_exp_c 365" ) ) ,
) ,
)
t . editMessageCallbackTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) , inlineKeyboard )
2025-05-06 16:27:17 +00:00
case "add_client_ch_default_ip_limit" :
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancel" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_default_ip_limit" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.unlimited" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_c 0" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.custom" ) ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_in 0" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "1" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_c 1" ) ) ,
tu . InlineKeyboardButton ( "2" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_c 2" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "3" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_c 3" ) ) ,
tu . InlineKeyboardButton ( "4" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_c 4" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "5" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_c 5" ) ) ,
tu . InlineKeyboardButton ( "6" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_c 6" ) ) ,
tu . InlineKeyboardButton ( "7" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_c 7" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( "8" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_c 8" ) ) ,
tu . InlineKeyboardButton ( "9" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_c 9" ) ) ,
tu . InlineKeyboardButton ( "10" ) . WithCallbackData ( t . encodeQuery ( "add_client_ip_limit_c 10" ) ) ,
) ,
)
t . editMessageCallbackTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) , inlineKeyboard )
2025-03-26 18:16:35 +00:00
case "add_client_default_info" :
t . deleteMessageTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) )
t . SendMsgToTgbotDeleteAfter ( chatId , t . I18nBot ( "tgbot.messages.using_default_value" ) , 3 , tu . ReplyKeyboardRemove ( ) )
delete ( userStates , chatId )
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>
2026-05-18 00:11:30 +00:00
t . addClient ( chatId , t . BuildClientDraftMessage ( ) )
2025-03-26 18:16:35 +00:00
case "add_client_cancel" :
delete ( userStates , chatId )
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>
2026-05-18 00:11:30 +00:00
receiver_inbound_ID = 0
receiver_inbound_IDs = nil
2025-03-26 18:16:35 +00:00
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 ( )
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>
2026-05-18 00:11:30 +00:00
message_text := t . BuildClientDraftMessage ( )
2025-04-06 22:45:52 +00:00
t . addClient ( chatId , message_text , messageId )
2025-03-26 18:16:35 +00:00
t . sendCallbackAnswerTgBot ( callbackQuery . ID , t . I18nBot ( "tgbot.answers.canceled" , "Email==" + client_Email ) )
2025-05-06 16:27:17 +00:00
case "add_client_default_ip_limit" :
messageId := callbackQuery . Message . GetMessageID ( )
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>
2026-05-18 00:11:30 +00:00
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 ( )
2025-05-06 16:27:17 +00:00
if err != nil {
t . sendCallbackAnswerTgBot ( callbackQuery . ID , err . Error ( ) )
return
}
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>
2026-05-18 00:11:30 +00:00
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" ) )
2025-08-17 11:37:49 +00:00
return
}
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>
2026-05-18 00:11:30 +00:00
message_text := t . BuildClientDraftMessage ( )
t . deleteMessageTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) )
t . addClient ( chatId , message_text )
2025-03-26 18:16:35 +00:00
case "add_client_submit_disable" :
client_Enable = false
_ , err := t . SubmitAddClient ( )
if err != nil {
errorMessage := fmt . Sprintf ( "%v" , err )
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.messages.error_add_client" , "error==" + errorMessage ) , tu . ReplyKeyboardRemove ( ) )
} else {
t . deleteMessageTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) )
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.successfulOperation" ) , tu . ReplyKeyboardRemove ( ) )
2026-03-17 21:09:49 +00:00
t . sendClientIndividualLinks ( chatId , client_Email )
t . sendClientQRLinks ( chatId , client_Email )
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>
2026-05-18 00:11:30 +00:00
receiver_inbound_ID = 0
receiver_inbound_IDs = nil
2025-03-26 18:16:35 +00:00
}
2025-04-16 08:16:55 +00:00
case "add_client_submit_enable" :
client_Enable = true
_ , err := t . SubmitAddClient ( )
if err != nil {
errorMessage := fmt . Sprintf ( "%v" , err )
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.messages.error_add_client" , "error==" + errorMessage ) , tu . ReplyKeyboardRemove ( ) )
} else {
t . deleteMessageTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) )
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.successfulOperation" ) , tu . ReplyKeyboardRemove ( ) )
2026-03-17 21:09:49 +00:00
t . sendClientIndividualLinks ( chatId , client_Email )
t . sendClientQRLinks ( chatId , client_Email )
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>
2026-05-18 00:11:30 +00:00
receiver_inbound_ID = 0
receiver_inbound_IDs = nil
2025-04-16 08:16:55 +00:00
}
2025-05-06 16:27:17 +00:00
case "reset_all_traffics_cancel" :
t . deleteMessageTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) )
t . SendMsgToTgbotDeleteAfter ( chatId , t . I18nBot ( "tgbot.messages.cancel" ) , 1 , tu . ReplyKeyboardRemove ( ) )
case "reset_all_traffics" :
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancelReset" ) ) . WithCallbackData ( t . encodeQuery ( "reset_all_traffics_cancel" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.confirmResetTraffic" ) ) . WithCallbackData ( t . encodeQuery ( "reset_all_traffics_c" ) ) ,
) ,
)
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.messages.AreYouSure" ) , inlineKeyboard )
case "reset_all_traffics_c" :
t . deleteMessageTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) )
emails , err := t . inboundService . getAllEmails ( )
if err != nil {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.errorOperation" ) , tu . ReplyKeyboardRemove ( ) )
return
}
for _ , email := range emails {
err := t . inboundService . ResetClientTrafficByEmail ( email )
if err == nil {
msg := t . I18nBot ( "tgbot.messages.SuccessResetTraffic" , "ClientEmail==" + email )
t . SendMsgToTgbot ( chatId , msg , tu . ReplyKeyboardRemove ( ) )
} else {
msg := t . I18nBot ( "tgbot.messages.FailedResetTraffic" , "ClientEmail==" + email , "ErrorMessage==" + err . Error ( ) )
t . SendMsgToTgbot ( chatId , msg , tu . ReplyKeyboardRemove ( ) )
}
}
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.messages.FinishProcess" ) , tu . ReplyKeyboardRemove ( ) )
case "get_sorted_traffic_usage_report" :
t . deleteMessageTgBot ( chatId , callbackQuery . Message . GetMessageID ( ) )
emails , err := t . inboundService . getAllEmails ( )
if err != nil {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.errorOperation" ) , tu . ReplyKeyboardRemove ( ) )
return
}
valid_emails , extra_emails , err := t . inboundService . FilterAndSortClientEmails ( emails )
2025-08-17 11:37:49 +00:00
if err != nil {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.errorOperation" ) , tu . ReplyKeyboardRemove ( ) )
return
}
2025-05-06 16:27:17 +00:00
for _ , valid_emails := range valid_emails {
traffic , err := t . inboundService . GetClientTrafficByEmail ( valid_emails )
if err != nil {
logger . Warning ( err )
msg := t . I18nBot ( "tgbot.wentWrong" )
t . SendMsgToTgbot ( chatId , msg )
continue
}
if traffic == nil {
msg := t . I18nBot ( "tgbot.noResult" )
t . SendMsgToTgbot ( chatId , msg )
continue
}
output := t . clientInfoMsg ( traffic , false , false , false , false , true , false )
t . SendMsgToTgbot ( chatId , output , tu . ReplyKeyboardRemove ( ) )
}
for _ , extra_emails := range extra_emails {
msg := fmt . Sprintf ( "📧 %s\n%s" , extra_emails , t . I18nBot ( "tgbot.noResult" ) )
t . SendMsgToTgbot ( chatId , msg , tu . ReplyKeyboardRemove ( ) )
}
2025-09-18 20:06:01 +00:00
default :
if after , ok := strings . CutPrefix ( callbackQuery . Data , "client_sub_links " ) ; ok {
email := after
t . sendClientSubLinks ( chatId , email )
return
}
if after , ok := strings . CutPrefix ( callbackQuery . Data , "client_individual_links " ) ; ok {
email := after
t . sendClientIndividualLinks ( chatId , email )
return
}
if after , ok := strings . CutPrefix ( callbackQuery . Data , "client_qr_links " ) ; ok {
email := after
t . sendClientQRLinks ( chatId , email )
return
}
2025-03-26 18:16:35 +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>
2026-05-18 00:11:30 +00:00
// 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 :
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" ) )
}
2025-03-26 18:16:35 +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>
2026-05-18 00:11:30 +00:00
traffic := "♾️ Unlimited(Reset)"
if client_TotalGB > 0 {
traffic = common . FormatTraffic ( client_TotalGB )
2025-03-26 18:16:35 +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>
2026-05-18 00:11:30 +00:00
ipLimit := "♾️ Unlimited(Reset)"
if client_LimitIP > 0 {
ipLimit = fmt . Sprint ( client_LimitIP )
2025-05-06 16:27: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>
2026-05-18 00:11:30 +00:00
attached := t . describeAttachedInbounds ( receiver_inbound_IDs )
if attached == "" {
attached = "—"
}
2025-03-26 18:16:35 +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>
2026-05-18 00:11:30 +00:00
comment := client_Comment
if comment == "" {
comment = "—"
}
2025-03-26 18:16:35 +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>
2026-05-18 00:11:30 +00:00
tgID := client_TgID
if tgID == "" {
tgID = "—"
2025-04-06 22:45:52 +00:00
}
2025-03-26 18:16:35 +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>
2026-05-18 00:11:30 +00:00
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 ( )
2025-03-26 18:16:35 +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>
2026-05-18 00:11:30 +00:00
// 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 ""
}
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 )
2025-04-06 22:45:52 +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>
2026-05-18 00:11:30 +00:00
return strings . Join ( parts , ", " )
2025-03-26 18:16:35 +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>
2026-05-18 00:11:30 +00:00
// 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.
2025-03-26 18:16:35 +00:00
func ( t * Tgbot ) SubmitAddClient ( ) ( bool , error ) {
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>
2026-05-18 00:11:30 +00:00
inboundIDs := receiver_inbound_IDs
if len ( inboundIDs ) == 0 && receiver_inbound_ID > 0 {
inboundIDs = [ ] int { receiver_inbound_ID }
}
if len ( inboundIDs ) == 0 {
2025-03-26 18:16:35 +00:00
return false , errors . New ( t . I18nBot ( "tgbot.answers.getInboundsFailed" ) )
2023-03-17 16:07:49 +00:00
}
2025-03-26 18:16:35 +00:00
2026-05-17 08:29:25 +00:00
tgIDInt , _ := strconv . ParseInt ( client_TgID , 10 , 64 )
client := model . Client {
Email : client_Email ,
Enable : client_Enable ,
LimitIP : client_LimitIP ,
TotalGB : client_TotalGB ,
ExpiryTime : client_ExpiryTime ,
SubID : client_SubID ,
Comment : client_Comment ,
Reset : client_Reset ,
TgID : tgIDInt ,
2025-08-17 11:37:49 +00:00
}
2025-03-26 18:16:35 +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>
2026-05-18 00:11:30 +00:00
return t . clientService . Create ( & t . inboundService , & ClientCreatePayload {
Client : client ,
InboundIds : inboundIDs ,
} )
2023-03-17 16:07:49 +00:00
}
2025-09-20 07:35:50 +00:00
// checkAdmin checks if the given Telegram ID is an admin.
2023-03-17 16:07:49 +00:00
func checkAdmin ( tgId int64 ) bool {
2025-03-26 18:16:35 +00:00
for _ , adminId := range adminIds {
if adminId == tgId {
return true
}
}
return false
2023-03-17 16:07:49 +00:00
}
2025-09-20 07:35:50 +00:00
// SendAnswer sends a response message with an inline keyboard to the specified chat.
2023-03-17 16:07:49 +00:00
func ( t * Tgbot ) SendAnswer ( chatId int64 , msg string , isAdmin bool ) {
2023-05-14 15:20:01 +00:00
numericKeyboard := tu . InlineKeyboard (
2025-05-06 16:27:17 +00:00
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.SortedTrafficUsageReport" ) ) . WithCallbackData ( t . encodeQuery ( "get_sorted_traffic_usage_report" ) ) ,
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.serverUsage" ) ) . WithCallbackData ( t . encodeQuery ( "get_usage" ) ) ,
2025-05-06 16:27:17 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.ResetAllTraffics" ) ) . WithCallbackData ( t . encodeQuery ( "reset_all_traffics" ) ) ,
2024-01-01 15:07:56 +00:00
) ,
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.dbBackup" ) ) . WithCallbackData ( t . encodeQuery ( "get_backup" ) ) ,
2024-01-01 15:07:56 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.getBanLogs" ) ) . WithCallbackData ( t . encodeQuery ( "get_banlogs" ) ) ,
2023-03-17 16:07:49 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.getInbounds" ) ) . WithCallbackData ( t . encodeQuery ( "inbounds" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.depleteSoon" ) ) . WithCallbackData ( t . encodeQuery ( "deplete_soon" ) ) ,
2023-03-17 16:07:49 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.commands" ) ) . WithCallbackData ( t . encodeQuery ( "commands" ) ) ,
2024-01-01 15:07:56 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.onlines" ) ) . WithCallbackData ( t . encodeQuery ( "onlines" ) ) ,
2025-03-26 18:16:35 +00:00
) ,
tu . InlineKeyboardRow (
2024-08-18 21:30:56 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.allClients" ) ) . WithCallbackData ( t . encodeQuery ( "get_inbounds" ) ) ,
2025-03-26 18:16:35 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.addClient" ) ) . WithCallbackData ( t . encodeQuery ( "add_client" ) ) ,
2023-03-17 16:07:49 +00:00
) ,
2025-09-16 11:41:48 +00:00
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "pages.settings.subSettings" ) ) . WithCallbackData ( t . encodeQuery ( "admin_client_sub_links" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "subscription.individualLinks" ) ) . WithCallbackData ( t . encodeQuery ( "admin_client_individual_links" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "qrCode" ) ) . WithCallbackData ( t . encodeQuery ( "admin_client_qr_links" ) ) ,
) ,
2024-10-16 12:39:25 +00:00
// TODOOOOOOOOOOOOOO: Add restart button here.
2023-03-17 16:07:49 +00:00
)
2023-05-14 15:20:01 +00:00
numericKeyboardClient := tu . InlineKeyboard (
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.clientUsage" ) ) . WithCallbackData ( t . encodeQuery ( "client_traffic" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.commands" ) ) . WithCallbackData ( t . encodeQuery ( "client_commands" ) ) ,
2023-03-17 16:07:49 +00:00
) ,
2025-09-14 17:51:57 +00:00
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "pages.settings.subSettings" ) ) . WithCallbackData ( t . encodeQuery ( "client_sub_links" ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "subscription.individualLinks" ) ) . WithCallbackData ( t . encodeQuery ( "client_individual_links" ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "qrCode" ) ) . WithCallbackData ( t . encodeQuery ( "client_qr_links" ) ) ,
) ,
2023-03-17 16:07:49 +00:00
)
2023-05-20 23:00:26 +00:00
2023-05-20 15:09:01 +00:00
var ReplyMarkup telego . ReplyMarkup
2023-03-17 16:07:49 +00:00
if isAdmin {
2023-05-20 15:09:01 +00:00
ReplyMarkup = numericKeyboard
2023-03-17 16:07:49 +00:00
} else {
2023-05-20 15:09:01 +00:00
ReplyMarkup = numericKeyboardClient
2023-03-17 16:07:49 +00:00
}
2023-05-20 15:09:01 +00:00
t . SendMsgToTgbot ( chatId , msg , ReplyMarkup )
2023-03-17 16:07:49 +00:00
}
2025-09-20 07:35:50 +00:00
// SendMsgToTgbot sends a message to the Telegram bot with optional reply markup.
2023-05-14 18:37:49 +00:00
func ( t * Tgbot ) SendMsgToTgbot ( chatId int64 , msg string , replyMarkup ... telego . ReplyMarkup ) {
2023-05-13 10:01:46 +00:00
if ! isRunning {
return
}
2023-05-30 22:01:00 +00:00
2023-05-20 15:09:01 +00:00
if msg == "" {
logger . Info ( "[tgbot] message is empty!" )
return
}
2023-03-17 16:07:49 +00:00
var allMessages [ ] string
limit := 2000
2023-05-20 15:09:01 +00:00
2023-03-17 16:07:49 +00:00
// paging message if it is big
if len ( msg ) > limit {
2024-01-01 15:07:56 +00:00
messages := strings . Split ( msg , "\r\n\r\n" )
2023-03-17 16:07:49 +00:00
lastIndex := - 1
2023-05-20 15:09:01 +00:00
2023-03-17 16:07:49 +00:00
for _ , message := range messages {
if ( len ( allMessages ) == 0 ) || ( len ( allMessages [ lastIndex ] ) + len ( message ) > limit ) {
allMessages = append ( allMessages , message )
lastIndex ++
} else {
2024-01-01 15:07:56 +00:00
allMessages [ lastIndex ] += "\r\n\r\n" + message
2023-03-17 16:07:49 +00:00
}
}
2024-01-01 15:07:56 +00:00
if strings . TrimSpace ( allMessages [ len ( allMessages ) - 1 ] ) == "" {
allMessages = allMessages [ : len ( allMessages ) - 1 ]
}
2023-03-17 16:07:49 +00:00
} else {
allMessages = append ( allMessages , msg )
}
2024-01-01 15:07:56 +00:00
for n , message := range allMessages {
2023-05-14 15:20:01 +00:00
params := telego . SendMessageParams {
ChatID : tu . ID ( chatId ) ,
Text : message ,
ParseMode : "HTML" ,
}
2024-03-10 21:31:24 +00:00
// only add replyMarkup to last message
2024-01-01 15:07:56 +00:00
if len ( replyMarkup ) > 0 && n == ( len ( allMessages ) - 1 ) {
2023-05-14 18:37:49 +00:00
params . ReplyMarkup = replyMarkup [ 0 ]
2023-05-04 21:46:43 +00:00
}
2026-02-14 21:49:19 +00:00
// Retry logic with exponential backoff for connection errors
maxRetries := 3
for attempt := range maxRetries {
ctx , cancel := context . WithTimeout ( context . Background ( ) , 30 * time . Second )
_ , err := bot . SendMessage ( ctx , & params )
cancel ( )
if err == nil {
break // Success
}
// Check if error is a connection error
errStr := err . Error ( )
isConnectionError := strings . Contains ( errStr , "connection" ) ||
strings . Contains ( errStr , "timeout" ) ||
strings . Contains ( errStr , "closed" )
if isConnectionError && attempt < maxRetries - 1 {
// Exponential backoff: 1s, 2s, 4s
backoff := time . Duration ( 1 << uint ( attempt ) ) * time . Second
logger . Warningf ( "Connection error sending telegram message (attempt %d/%d), retrying in %v: %v" ,
attempt + 1 , maxRetries , backoff , err )
time . Sleep ( backoff )
} else {
logger . Warning ( "Error sending telegram message:" , err )
break
}
2023-03-17 16:07:49 +00:00
}
2026-02-14 21:49:19 +00:00
2025-09-21 17:27:05 +00:00
// Reduced delay to improve performance (only needed for rate limiting)
if n < len ( allMessages ) - 1 { // Only delay between messages, not after the last one
time . Sleep ( 100 * time . Millisecond )
}
2023-03-17 16:07:49 +00:00
}
}
2025-09-14 17:51:57 +00:00
// buildSubscriptionURLs builds the HTML sub page URL and JSON subscription URL for a client email
func ( t * Tgbot ) buildSubscriptionURLs ( email string ) ( string , string , error ) {
// Resolve subId from client email
traffic , client , err := t . inboundService . GetClientByEmail ( email )
_ = traffic
if err != nil || client == nil {
return "" , "" , errors . New ( "client not found" )
}
// Gather settings to construct absolute URLs
2026-01-19 11:33:17 +00:00
subURI , _ := t . settingService . GetSubURI ( )
subJsonURI , _ := t . settingService . GetSubJsonURI ( )
2025-09-14 17:51:57 +00:00
subDomain , _ := t . settingService . GetSubDomain ( )
subPort , _ := t . settingService . GetSubPort ( )
subPath , _ := t . settingService . GetSubPath ( )
subJsonPath , _ := t . settingService . GetSubJsonPath ( )
2025-09-18 11:56:04 +00:00
subJsonEnable , _ := t . settingService . GetSubJsonEnable ( )
2025-09-14 17:51:57 +00:00
subKeyFile , _ := t . settingService . GetSubKeyFile ( )
subCertFile , _ := t . settingService . GetSubCertFile ( )
tls := ( subKeyFile != "" && subCertFile != "" )
scheme := "http"
if tls {
scheme = "https"
}
// Fallbacks
if subDomain == "" {
// try panel domain, otherwise OS hostname
if d , err := t . settingService . GetWebDomain ( ) ; err == nil && d != "" {
subDomain = d
} else if hostname != "" {
subDomain = hostname
} else {
subDomain = "localhost"
}
}
host := subDomain
if ( subPort == 443 && tls ) || ( subPort == 80 && ! tls ) {
// standard ports: no port in host
} else {
host = fmt . Sprintf ( "%s:%d" , subDomain , subPort )
}
// Ensure paths
if ! strings . HasPrefix ( subPath , "/" ) {
subPath = "/" + subPath
}
if ! strings . HasSuffix ( subPath , "/" ) {
subPath = subPath + "/"
}
if ! strings . HasPrefix ( subJsonPath , "/" ) {
subJsonPath = "/" + subJsonPath
}
if ! strings . HasSuffix ( subJsonPath , "/" ) {
subJsonPath = subJsonPath + "/"
}
2026-01-19 11:33:17 +00:00
var subURL string
var subJsonURL string
// If pre-configured URIs are available, use them directly
if subURI != "" {
if ! strings . HasSuffix ( subURI , "/" ) {
2026-02-02 23:14:39 +00:00
subURI = subURI + "/"
2026-01-19 11:33:17 +00:00
}
2026-02-02 23:14:39 +00:00
subURL = fmt . Sprintf ( "%s%s" , subURI , client . SubID )
2026-01-19 11:33:17 +00:00
} else {
subURL = fmt . Sprintf ( "%s://%s%s%s" , scheme , host , subPath , client . SubID )
}
if subJsonURI != "" {
if ! strings . HasSuffix ( subJsonURI , "/" ) {
subJsonURI = subJsonURI + "/"
}
2026-02-02 23:14:39 +00:00
subJsonURL = fmt . Sprintf ( "%s%s" , subJsonURI , client . SubID )
2026-01-19 11:33:17 +00:00
} else {
subJsonURL = fmt . Sprintf ( "%s://%s%s%s" , scheme , host , subJsonPath , client . SubID )
}
2025-09-18 11:56:04 +00:00
if ! subJsonEnable {
subJsonURL = ""
}
2025-09-14 17:51:57 +00:00
return subURL , subJsonURL , nil
}
2025-09-20 07:35:50 +00:00
// sendClientSubLinks sends the subscription links for the client to the chat.
2025-09-14 17:51:57 +00:00
func ( t * Tgbot ) sendClientSubLinks ( chatId int64 , email string ) {
subURL , subJsonURL , err := t . buildSubscriptionURLs ( email )
if err != nil {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.errorOperation" ) + "\r\n" + err . Error ( ) )
return
}
2025-09-18 11:56:04 +00:00
msg := "Subscription URL:\r\n<code>" + subURL + "</code>"
if subJsonURL != "" {
msg += "\r\n\r\nJSON URL:\r\n<code>" + subJsonURL + "</code>"
}
2025-09-14 17:51:57 +00:00
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
2025-09-16 11:41:48 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "subscription.individualLinks" ) ) . WithCallbackData ( t . encodeQuery ( "client_individual_links " + email ) ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "qrCode" ) ) . WithCallbackData ( t . encodeQuery ( "client_qr_links " + email ) ) ,
2025-09-14 17:51:57 +00:00
) ,
)
t . SendMsgToTgbot ( chatId , msg , inlineKeyboard )
}
// sendClientIndividualLinks fetches the subscription content (individual links) and sends it to the user
func ( t * Tgbot ) sendClientIndividualLinks ( chatId int64 , email string ) {
// Build the HTML sub page URL; we'll call it with header Accept to get raw content
subURL , _ , err := t . buildSubscriptionURLs ( email )
if err != nil {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.errorOperation" ) + "\r\n" + err . Error ( ) )
return
}
// Try to fetch raw subscription links. Prefer plain text response.
req , err := http . NewRequest ( "GET" , subURL , nil )
if err != nil {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.errorOperation" ) + "\r\n" + err . Error ( ) )
return
}
// Force plain text to avoid HTML page; controller respects Accept header
req . Header . Set ( "Accept" , "text/plain, */*;q=0.1" )
2025-09-21 17:27:05 +00:00
// Use optimized client with connection pooling
2025-09-14 17:51:57 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 10 * time . Second )
defer cancel ( )
req = req . WithContext ( ctx )
2025-09-21 17:27:05 +00:00
resp , err := optimizedHTTPClient . Do ( req )
2025-09-14 17:51:57 +00:00
if err != nil {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.errorOperation" ) + "\r\n" + err . Error ( ) )
return
}
defer resp . Body . Close ( )
bodyBytes , err := io . ReadAll ( resp . Body )
if err != nil {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.errorOperation" ) + "\r\n" + err . Error ( ) )
return
}
// If service is configured to encode (Base64), decode it
encoded , _ := t . settingService . GetSubEncrypt ( )
var content string
if encoded {
decoded , err := base64 . StdEncoding . DecodeString ( string ( bodyBytes ) )
if err != nil {
// fallback to raw text
content = string ( bodyBytes )
} else {
content = string ( decoded )
}
} else {
content = string ( bodyBytes )
}
// Normalize line endings and trim
lines := strings . Split ( strings . ReplaceAll ( content , "\r\n" , "\n" ) , "\n" )
var cleaned [ ] string
for _ , l := range lines {
l = strings . TrimSpace ( l )
if l != "" {
cleaned = append ( cleaned , l )
}
}
if len ( cleaned ) == 0 {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.noResult" ) )
return
}
// Send in chunks to respect message length; use monospace formatting
const maxPerMessage = 50
for i := 0 ; i < len ( cleaned ) ; i += maxPerMessage {
j := i + maxPerMessage
if j > len ( cleaned ) {
j = len ( cleaned )
}
chunk := cleaned [ i : j ]
msg := t . I18nBot ( "subscription.individualLinks" ) + ":\r\n"
for _ , link := range chunk {
// wrap each link in <code>
msg += "<code>" + link + "</code>\r\n"
}
t . SendMsgToTgbot ( chatId , msg )
}
}
// sendClientQRLinks generates QR images for subscription URL, JSON URL, and a few individual links, then sends them
func ( t * Tgbot ) sendClientQRLinks ( chatId int64 , email string ) {
subURL , subJsonURL , err := t . buildSubscriptionURLs ( email )
if err != nil {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.errorOperation" ) + "\r\n" + err . Error ( ) )
return
}
// Helper to create QR PNG bytes from content
createQR := func ( content string , size int ) ( [ ] byte , error ) {
if size <= 0 {
size = 256
}
return qrcode . Encode ( content , qrcode . Medium , size )
}
// Inform user
t . SendMsgToTgbot ( chatId , "QRCode" + ":" )
// Send sub URL QR (filename: sub.png)
if png , err := createQR ( subURL , 320 ) ; err == nil {
document := tu . Document (
tu . ID ( chatId ) ,
tu . FileFromBytes ( png , "sub.png" ) ,
)
_ , _ = bot . SendDocument ( context . Background ( ) , document )
} else {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.errorOperation" ) + "\r\n" + err . Error ( ) )
}
2025-09-18 11:56:04 +00:00
// Send JSON URL QR (filename: subjson.png) when available
if subJsonURL != "" {
if png , err := createQR ( subJsonURL , 320 ) ; err == nil {
document := tu . Document (
tu . ID ( chatId ) ,
tu . FileFromBytes ( png , "subjson.png" ) ,
)
_ , _ = bot . SendDocument ( context . Background ( ) , document )
} else {
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.errorOperation" ) + "\r\n" + err . Error ( ) )
}
2025-09-14 17:51:57 +00:00
}
// Also generate a few individual links' QRs (first up to 5)
subPageURL := subURL
req , err := http . NewRequest ( "GET" , subPageURL , nil )
if err == nil {
req . Header . Set ( "Accept" , "text/plain, */*;q=0.1" )
ctx , cancel := context . WithTimeout ( context . Background ( ) , 10 * time . Second )
defer cancel ( )
req = req . WithContext ( ctx )
2025-09-21 17:27:05 +00:00
if resp , err := optimizedHTTPClient . Do ( req ) ; err == nil {
2025-09-14 17:51:57 +00:00
body , _ := io . ReadAll ( resp . Body )
_ = resp . Body . Close ( )
encoded , _ := t . settingService . GetSubEncrypt ( )
var content string
if encoded {
if dec , err := base64 . StdEncoding . DecodeString ( string ( body ) ) ; err == nil {
content = string ( dec )
} else {
content = string ( body )
}
} else {
content = string ( body )
}
lines := strings . Split ( strings . ReplaceAll ( content , "\r\n" , "\n" ) , "\n" )
var cleaned [ ] string
for _ , l := range lines {
l = strings . TrimSpace ( l )
if l != "" {
cleaned = append ( cleaned , l )
}
}
if len ( cleaned ) > 0 {
max := min ( len ( cleaned ) , 5 )
for i := range max {
if png , err := createQR ( cleaned [ i ] , 320 ) ; err == nil {
// Use the email as filename for individual link QR
filename := email + ".png"
document := tu . Document (
tu . ID ( chatId ) ,
tu . FileFromBytes ( png , filename ) ,
)
_ , _ = bot . SendDocument ( context . Background ( ) , document )
2025-09-21 17:27:05 +00:00
// Reduced delay for better performance
if i < max - 1 { // Only delay between documents, not after the last one
time . Sleep ( 50 * time . Millisecond )
}
2025-09-14 17:51:57 +00:00
}
}
}
}
}
}
2025-09-20 07:35:50 +00:00
// SendMsgToTgbotAdmins sends a message to all admin Telegram chats.
2024-01-01 15:07:56 +00:00
func ( t * Tgbot ) SendMsgToTgbotAdmins ( msg string , replyMarkup ... telego . ReplyMarkup ) {
if len ( replyMarkup ) > 0 {
for _ , adminId := range adminIds {
t . SendMsgToTgbot ( adminId , msg , replyMarkup [ 0 ] )
}
} else {
for _ , adminId := range adminIds {
t . SendMsgToTgbot ( adminId , msg )
}
2023-03-17 16:07:49 +00:00
}
}
2025-09-20 07:35:50 +00:00
// SendReport sends a periodic report to admin chats.
2023-03-17 16:07:49 +00:00
func ( t * Tgbot ) SendReport ( ) {
runTime , err := t . settingService . GetTgbotRuntime ( )
if err == nil && len ( runTime ) > 0 {
2023-05-20 23:00:26 +00:00
msg := ""
msg += t . I18nBot ( "tgbot.messages.report" , "RunTime==" + runTime )
msg += t . I18nBot ( "tgbot.messages.datetime" , "DateTime==" + time . Now ( ) . Format ( "2006-01-02 15:04:05" ) )
t . SendMsgToTgbotAdmins ( msg )
2023-03-17 16:07:49 +00:00
}
2023-05-20 23:00:26 +00:00
2024-05-14 12:00:10 +00:00
info := t . sendServerUsage ( )
2023-03-17 16:07:49 +00:00
t . SendMsgToTgbotAdmins ( info )
2023-05-20 23:00:26 +00:00
2024-01-01 15:07:56 +00:00
t . sendExhaustedToAdmins ( )
t . notifyExhausted ( )
2023-05-20 23:00:26 +00:00
2023-03-17 16:07:49 +00:00
backupEnable , err := t . settingService . GetTgBotBackup ( )
if err == nil && backupEnable {
2023-05-20 23:00:26 +00:00
t . SendBackupToAdmins ( )
2023-03-17 16:07:49 +00:00
}
}
2025-09-20 07:35:50 +00:00
// SendBackupToAdmins sends a database backup to admin chats.
2023-05-20 23:00:26 +00:00
func ( t * Tgbot ) SendBackupToAdmins ( ) {
if ! t . IsRunning ( ) {
return
}
2026-02-14 21:31:41 +00:00
for i , adminId := range adminIds {
2023-05-18 21:01:05 +00:00
t . sendBackup ( int64 ( adminId ) )
2026-02-14 21:31:41 +00:00
// Add delay between sends to avoid Telegram rate limits
if i < len ( adminIds ) - 1 {
time . Sleep ( 1 * time . Second )
}
2023-05-18 21:01:05 +00:00
}
}
2025-09-20 07:35:50 +00:00
// sendExhaustedToAdmins sends notifications about exhausted clients to admins.
2024-01-01 15:07:56 +00:00
func ( t * Tgbot ) sendExhaustedToAdmins ( ) {
if ! t . IsRunning ( ) {
return
}
for _ , adminId := range adminIds {
t . getExhausted ( int64 ( adminId ) )
}
}
2025-09-20 07:35:50 +00:00
// getServerUsage retrieves and formats server usage information.
2024-05-14 12:00:10 +00:00
func ( t * Tgbot ) getServerUsage ( chatId int64 , messageID ... int ) string {
info := t . prepareServerUsageInfo ( )
keyboard := tu . InlineKeyboard ( tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.refresh" ) ) . WithCallbackData ( t . encodeQuery ( "usage_refresh" ) ) ) )
if len ( messageID ) > 0 {
t . editMessageTgBot ( chatId , messageID [ 0 ] , info , keyboard )
} else {
t . SendMsgToTgbot ( chatId , info , keyboard )
}
return info
}
2024-07-07 09:55:59 +00:00
// Send server usage without an inline keyboard
2024-05-14 12:00:10 +00:00
func ( t * Tgbot ) sendServerUsage ( ) string {
info := t . prepareServerUsageInfo ( )
return info
}
2025-09-20 07:35:50 +00:00
// prepareServerUsageInfo prepares the server usage information string.
2024-05-14 12:00:10 +00:00
func ( t * Tgbot ) prepareServerUsageInfo ( ) string {
2025-09-21 17:27:05 +00:00
// Check if we have cached data first
if cachedStats , found := t . getCachedServerStats ( ) ; found {
return cachedStats
}
2025-09-21 22:20:05 +00:00
2023-05-20 23:00:26 +00:00
info , ipv4 , ipv6 := "" , "" , ""
2024-04-29 06:44:16 +00:00
2025-09-21 17:27:05 +00:00
// get latest status of server with caching
if cachedStatus , found := t . getCachedStatus ( ) ; found {
t . lastStatus = cachedStatus
} else {
t . lastStatus = t . serverService . GetStatus ( t . lastStatus )
t . setCachedStatus ( t . lastStatus )
}
2024-04-29 06:44:16 +00:00
onlines := p . GetOnlineClients ( )
2023-05-20 23:00:26 +00:00
info += t . I18nBot ( "tgbot.messages.hostname" , "Hostname==" + hostname )
info += t . I18nBot ( "tgbot.messages.version" , "Version==" + config . GetVersion ( ) )
2024-04-29 06:44:16 +00:00
info += t . I18nBot ( "tgbot.messages.xrayVersion" , "XrayVersion==" + fmt . Sprint ( t . lastStatus . Xray . Version ) )
2023-05-20 23:00:26 +00:00
// get ip address
2023-03-17 16:07:49 +00:00
netInterfaces , err := net . Interfaces ( )
if err != nil {
2023-05-20 23:00:26 +00:00
logger . Error ( "net.Interfaces failed, err: " , err . Error ( ) )
info += t . I18nBot ( "tgbot.messages.ip" , "IP==" + t . I18nBot ( "tgbot.unknown" ) )
2024-01-01 15:07:56 +00:00
info += "\r\n"
2023-03-17 16:07:49 +00:00
} else {
2026-03-04 12:05:29 +00:00
for i := range netInterfaces {
2023-03-17 16:07:49 +00:00
if ( netInterfaces [ i ] . Flags & net . FlagUp ) != 0 {
addrs , _ := netInterfaces [ i ] . Addrs ( )
for _ , address := range addrs {
if ipnet , ok := address . ( * net . IPNet ) ; ok && ! ipnet . IP . IsLoopback ( ) {
if ipnet . IP . To4 ( ) != nil {
2023-05-20 23:00:26 +00:00
ipv4 += ipnet . IP . String ( ) + " "
2023-03-17 16:07:49 +00:00
} else if ipnet . IP . To16 ( ) != nil && ! ipnet . IP . IsLinkLocalUnicast ( ) {
ipv6 += ipnet . IP . String ( ) + " "
}
}
}
}
}
2023-05-20 23:00:26 +00:00
info += t . I18nBot ( "tgbot.messages.ipv4" , "IPv4==" + ipv4 )
info += t . I18nBot ( "tgbot.messages.ipv6" , "IPv6==" + ipv6 )
2023-03-17 16:07:49 +00:00
}
2023-05-20 23:00:26 +00:00
info += t . I18nBot ( "tgbot.messages.serverUpTime" , "UpTime==" + strconv . FormatUint ( t . lastStatus . Uptime / 86400 , 10 ) , "Unit==" + t . I18nBot ( "tgbot.days" ) )
info += t . I18nBot ( "tgbot.messages.serverLoad" , "Load1==" + strconv . FormatFloat ( t . lastStatus . Loads [ 0 ] , 'f' , 2 , 64 ) , "Load2==" + strconv . FormatFloat ( t . lastStatus . Loads [ 1 ] , 'f' , 2 , 64 ) , "Load3==" + strconv . FormatFloat ( t . lastStatus . Loads [ 2 ] , 'f' , 2 , 64 ) )
info += t . I18nBot ( "tgbot.messages.serverMemory" , "Current==" + common . FormatTraffic ( int64 ( t . lastStatus . Mem . Current ) ) , "Total==" + common . FormatTraffic ( int64 ( t . lastStatus . Mem . Total ) ) )
2024-01-01 15:07:56 +00:00
info += t . I18nBot ( "tgbot.messages.onlinesCount" , "Count==" + fmt . Sprint ( len ( onlines ) ) )
2023-05-20 23:00:26 +00:00
info += t . I18nBot ( "tgbot.messages.tcpCount" , "Count==" + strconv . Itoa ( t . lastStatus . TcpCount ) )
info += t . I18nBot ( "tgbot.messages.udpCount" , "Count==" + strconv . Itoa ( t . lastStatus . UdpCount ) )
info += t . I18nBot ( "tgbot.messages.traffic" , "Total==" + common . FormatTraffic ( int64 ( t . lastStatus . NetTraffic . Sent + t . lastStatus . NetTraffic . Recv ) ) , "Upload==" + common . FormatTraffic ( int64 ( t . lastStatus . NetTraffic . Sent ) ) , "Download==" + common . FormatTraffic ( int64 ( t . lastStatus . NetTraffic . Recv ) ) )
info += t . I18nBot ( "tgbot.messages.xrayStatus" , "State==" + fmt . Sprint ( t . lastStatus . Xray . State ) )
2025-09-21 22:20:05 +00:00
2025-09-21 17:27:05 +00:00
// Cache the complete server stats
t . setCachedServerStats ( info )
2025-09-21 22:20:05 +00:00
2023-03-17 16:07:49 +00:00
return info
}
2025-09-20 07:35:50 +00:00
// UserLoginNotify sends a notification about user login attempts to admins.
2026-05-07 21:36:11 +00:00
func ( t * Tgbot ) UserLoginNotify ( attempt LoginAttempt ) {
2023-05-20 15:09:01 +00:00
if ! t . IsRunning ( ) {
return
}
2026-05-07 21:36:11 +00:00
if attempt . Username == "" || attempt . IP == "" || attempt . Time == "" {
2023-05-20 15:09:01 +00:00
logger . Warning ( "UserLoginNotify failed, invalid info!" )
2023-03-17 16:07:49 +00:00
return
}
2023-05-20 15:09:01 +00:00
2023-06-17 15:41:16 +00:00
loginNotifyEnabled , err := t . settingService . GetTgBotLoginNotify ( )
if err != nil || ! loginNotifyEnabled {
return
}
2023-05-20 15:09:01 +00:00
msg := ""
2026-05-07 21:36:11 +00:00
switch attempt . Status {
2025-08-17 11:37:49 +00:00
case LoginSuccess :
2023-05-20 23:00:26 +00:00
msg += t . I18nBot ( "tgbot.messages.loginSuccess" )
2024-07-03 19:53:45 +00:00
msg += t . I18nBot ( "tgbot.messages.hostname" , "Hostname==" + hostname )
2025-08-17 11:37:49 +00:00
case LoginFail :
2023-05-20 23:00:26 +00:00
msg += t . I18nBot ( "tgbot.messages.loginFailed" )
2024-07-03 19:53:45 +00:00
msg += t . I18nBot ( "tgbot.messages.hostname" , "Hostname==" + hostname )
2026-05-07 21:36:11 +00:00
if attempt . Reason != "" {
msg += t . I18nBot ( "tgbot.messages.reason" , "Reason==" + attempt . Reason )
}
2023-03-17 16:07:49 +00:00
}
2026-05-07 21:36:11 +00:00
msg += t . I18nBot ( "tgbot.messages.username" , "Username==" + attempt . Username )
msg += t . I18nBot ( "tgbot.messages.ip" , "IP==" + attempt . IP )
msg += t . I18nBot ( "tgbot.messages.time" , "Time==" + attempt . Time )
2023-03-17 16:07:49 +00:00
t . SendMsgToTgbotAdmins ( msg )
}
2025-09-20 07:35:50 +00:00
// getInboundUsages retrieves and formats inbound usage information.
2023-03-17 16:07:49 +00:00
func ( t * Tgbot ) getInboundUsages ( ) string {
2026-03-04 12:05:29 +00:00
var info strings . Builder
2024-07-07 09:55:59 +00:00
inbounds , err := t . inboundService . GetAllInbounds ( )
2023-03-17 16:07:49 +00:00
if err != nil {
logger . Warning ( "GetAllInbounds run failed:" , err )
2026-03-04 12:05:29 +00:00
info . WriteString ( t . I18nBot ( "tgbot.answers.getInboundsFailed" ) )
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>
2026-05-18 00:11:30 +00:00
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 ) ) )
clients , listErr := t . clientService . ListForInbound ( nil , inbound . Id )
if listErr == nil {
info . WriteString ( fmt . Sprintf ( "👥 Clients: %d\r\n" , len ( clients ) ) )
2023-03-17 16:07:49 +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>
2026-05-18 00:11:30 +00:00
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" )
2023-03-17 16:07:49 +00:00
}
2026-03-04 12:05:29 +00:00
return info . String ( )
2023-03-17 16:07:49 +00:00
}
2025-09-20 07:35:50 +00:00
// getInbounds creates an inline keyboard with all inbounds.
2024-08-18 21:30:56 +00:00
func ( t * Tgbot ) getInbounds ( ) ( * telego . InlineKeyboardMarkup , error ) {
inbounds , err := t . inboundService . GetAllInbounds ( )
2025-03-26 18:16:35 +00:00
if err != nil {
logger . Warning ( "GetAllInbounds run failed:" , err )
return nil , errors . New ( t . I18nBot ( "tgbot.answers.getInboundsFailed" ) )
}
if len ( inbounds ) == 0 {
logger . Warning ( "No inbounds found" )
return nil , errors . New ( t . I18nBot ( "tgbot.answers.getInboundsFailed" ) )
}
2024-08-18 21:30:56 +00:00
var buttons [ ] telego . InlineKeyboardButton
2025-03-26 18:16:35 +00:00
for _ , inbound := range inbounds {
status := "❌"
if inbound . Enable {
status = "✅"
}
2025-04-06 22:45:52 +00:00
callbackData := t . encodeQuery ( fmt . Sprintf ( "%s %d" , "get_clients" , inbound . Id ) )
2025-03-26 18:16:35 +00:00
buttons = append ( buttons , tu . InlineKeyboardButton ( fmt . Sprintf ( "%v - %v" , inbound . Remark , status ) ) . WithCallbackData ( callbackData ) )
}
2024-08-18 21:30:56 +00:00
2025-03-26 18:16:35 +00:00
cols := 1
if len ( buttons ) >= 6 {
cols = 2
}
keyboard := tu . InlineKeyboardGrid ( tu . InlineKeyboardCols ( cols , buttons ... ) )
return keyboard , nil
}
2025-09-20 07:35:50 +00:00
// getInboundsFor builds an inline keyboard of inbounds for a custom next action.
2025-09-16 11:41:48 +00:00
func ( t * Tgbot ) getInboundsFor ( nextAction string ) ( * 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 {
logger . Warning ( "No inbounds found" )
return nil , errors . New ( t . I18nBot ( "tgbot.answers.getInboundsFailed" ) )
}
var buttons [ ] telego . InlineKeyboardButton
for _ , inbound := range inbounds {
status := "❌"
if inbound . Enable {
status = "✅"
}
callbackData := t . encodeQuery ( fmt . Sprintf ( "%s %d" , nextAction , inbound . Id ) )
buttons = append ( buttons , tu . InlineKeyboardButton ( fmt . Sprintf ( "%v - %v" , inbound . Remark , status ) ) . WithCallbackData ( callbackData ) )
}
cols := 1
if len ( buttons ) >= 6 {
cols = 2
}
keyboard := tu . InlineKeyboardGrid ( tu . InlineKeyboardCols ( cols , buttons ... ) )
return keyboard , nil
}
// getInboundClientsFor lists clients of an inbound with a specific action prefix to be appended with email
func ( t * Tgbot ) getInboundClientsFor ( inboundID int , action string ) ( * telego . InlineKeyboardMarkup , error ) {
inbound , err := t . inboundService . GetInbound ( inboundID )
if err != nil {
logger . Warning ( "getInboundClientsFor run failed:" , err )
return nil , errors . New ( t . I18nBot ( "tgbot.answers.getInboundsFailed" ) )
}
clients , err := t . inboundService . GetClients ( inbound )
var buttons [ ] telego . InlineKeyboardButton
if err != nil {
logger . Warning ( "GetInboundClients run failed:" , err )
return nil , errors . New ( t . I18nBot ( "tgbot.answers.getInboundsFailed" ) )
} else {
if len ( clients ) > 0 {
for _ , client := range clients {
buttons = append ( buttons , tu . InlineKeyboardButton ( client . Email ) . WithCallbackData ( t . encodeQuery ( action + " " + client . Email ) ) )
}
} else {
return nil , errors . New ( t . I18nBot ( "tgbot.answers.getClientsFailed" ) )
}
}
cols := 0
if len ( buttons ) < 6 {
cols = 3
} else {
cols = 2
}
keyboard := tu . InlineKeyboardGrid ( tu . InlineKeyboardCols ( cols , buttons ... ) )
return keyboard , nil
}
2025-09-20 07:35:50 +00:00
// getInboundsAddClient creates an inline keyboard for adding clients to inbounds.
2025-03-26 18:16:35 +00:00
func ( t * Tgbot ) getInboundsAddClient ( ) ( * telego . InlineKeyboardMarkup , error ) {
inbounds , err := t . inboundService . GetAllInbounds ( )
2024-08-18 21:30:56 +00:00
if err != nil {
logger . Warning ( "GetAllInbounds run failed:" , err )
return nil , errors . New ( t . I18nBot ( "tgbot.answers.getInboundsFailed" ) )
2025-03-26 18:16:35 +00:00
}
if len ( inbounds ) == 0 {
logger . Warning ( "No inbounds found" )
return nil , errors . New ( t . I18nBot ( "tgbot.answers.getInboundsFailed" ) )
}
excludedProtocols := map [ model . Protocol ] bool {
2025-09-09 11:57:40 +00:00
model . Tunnel : true ,
model . Mixed : true ,
2025-04-06 22:45:52 +00:00
model . WireGuard : true ,
model . HTTP : true ,
}
2025-03-26 18:16:35 +00:00
var buttons [ ] telego . InlineKeyboardButton
for _ , inbound := range inbounds {
if excludedProtocols [ inbound . Protocol ] {
continue
2024-08-18 21:30:56 +00:00
}
2025-03-26 18:16:35 +00:00
status := "❌"
if inbound . Enable {
status = "✅"
}
2025-04-06 22:45:52 +00:00
callbackData := t . encodeQuery ( fmt . Sprintf ( "%s %d" , "add_client_to" , inbound . Id ) )
2025-03-26 18:16:35 +00:00
buttons = append ( buttons , tu . InlineKeyboardButton ( fmt . Sprintf ( "%v - %v" , inbound . Remark , status ) ) . WithCallbackData ( callbackData ) )
2024-08-18 21:30:56 +00:00
}
2025-03-26 18:16:35 +00:00
cols := 1
if len ( buttons ) >= 6 {
2024-08-18 21:30:56 +00:00
cols = 2
}
2025-03-26 18:16:35 +00:00
2024-08-18 21:30:56 +00:00
keyboard := tu . InlineKeyboardGrid ( tu . InlineKeyboardCols ( cols , buttons ... ) )
return keyboard , nil
}
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>
2026-05-18 00:11:30 +00:00
// 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
}
2025-09-20 07:35:50 +00:00
// getInboundClients creates an inline keyboard with clients of a specific inbound.
2024-08-18 21:30:56 +00:00
func ( t * Tgbot ) getInboundClients ( id int ) ( * telego . InlineKeyboardMarkup , error ) {
inbound , err := t . inboundService . GetInbound ( id )
if err != nil {
logger . Warning ( "getIboundClients run failed:" , err )
return nil , errors . New ( t . I18nBot ( "tgbot.answers.getInboundsFailed" ) )
}
clients , err := t . inboundService . GetClients ( inbound )
var buttons [ ] telego . InlineKeyboardButton
if err != nil {
logger . Warning ( "GetInboundClients run failed:" , err )
return nil , errors . New ( t . I18nBot ( "tgbot.answers.getInboundsFailed" ) )
} else {
if len ( clients ) > 0 {
for _ , client := range clients {
buttons = append ( buttons , tu . InlineKeyboardButton ( client . Email ) . WithCallbackData ( t . encodeQuery ( "client_get_usage " + client . Email ) ) )
}
} else {
return nil , errors . New ( t . I18nBot ( "tgbot.answers.getClientsFailed" ) )
}
}
cols := 0
if len ( buttons ) < 6 {
cols = 3
} else {
cols = 2
}
keyboard := tu . InlineKeyboardGrid ( tu . InlineKeyboardCols ( cols , buttons ... ) )
return keyboard , nil
}
2025-09-20 07:35:50 +00:00
// clientInfoMsg formats client information message based on traffic and flags.
2024-03-10 21:31:24 +00:00
func ( t * Tgbot ) clientInfoMsg (
traffic * xray . ClientTraffic ,
printEnabled bool ,
printOnline bool ,
printActive bool ,
printDate bool ,
printTraffic bool ,
printRefreshed bool ,
) string {
2024-01-01 15:07:56 +00:00
now := time . Now ( ) . Unix ( )
expiryTime := ""
flag := false
diff := traffic . ExpiryTime / 1000 - now
if traffic . ExpiryTime == 0 {
expiryTime = t . I18nBot ( "tgbot.unlimited" )
} else if diff > 172800 || ! traffic . Enable {
expiryTime = time . Unix ( ( traffic . ExpiryTime / 1000 ) , 0 ) . Format ( "2006-01-02 15:04:05" )
2025-08-17 11:43:25 +00:00
if diff > 0 {
days := diff / 86400
hours := ( diff % 86400 ) / 3600
minutes := ( diff % 3600 ) / 60
remainingTime := ""
if days > 0 {
remainingTime += fmt . Sprintf ( "%d %s " , days , t . I18nBot ( "tgbot.days" ) )
}
if hours > 0 {
remainingTime += fmt . Sprintf ( "%d %s " , hours , t . I18nBot ( "tgbot.hours" ) )
}
if minutes > 0 {
remainingTime += fmt . Sprintf ( "%d %s" , minutes , t . I18nBot ( "tgbot.minutes" ) )
}
expiryTime += fmt . Sprintf ( " (%s)" , remainingTime )
}
2024-01-01 15:07:56 +00:00
} else if traffic . ExpiryTime < 0 {
expiryTime = fmt . Sprintf ( "%d %s" , traffic . ExpiryTime / - 86400000 , t . I18nBot ( "tgbot.days" ) )
flag = true
} else {
expiryTime = fmt . Sprintf ( "%d %s" , diff / 3600 , t . I18nBot ( "tgbot.hours" ) )
flag = true
}
total := ""
if traffic . Total == 0 {
total = t . I18nBot ( "tgbot.unlimited" )
} else {
total = common . FormatTraffic ( ( traffic . Total ) )
}
enabled := ""
refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 08:48:28 +00:00
isEnabled , err := t . clientService . checkIsEnabledByEmail ( & t . inboundService , traffic . Email )
2023-05-05 23:06:46 +00:00
if err != nil {
logger . Warning ( err )
2024-01-01 15:07:56 +00:00
enabled = t . I18nBot ( "tgbot.wentWrong" )
} else if isEnabled {
enabled = t . I18nBot ( "tgbot.messages.yes" )
} else {
enabled = t . I18nBot ( "tgbot.messages.no" )
2023-04-09 19:43:18 +00:00
}
2023-05-20 23:00:26 +00:00
2024-01-01 15:07:56 +00:00
active := ""
if traffic . Enable {
active = t . I18nBot ( "tgbot.messages.yes" )
} else {
active = t . I18nBot ( "tgbot.messages.no" )
}
status := t . I18nBot ( "tgbot.offline" )
2025-12-03 13:43:37 +00:00
isOnline := false
2024-01-01 15:07:56 +00:00
if p . IsRunning ( ) {
2026-03-04 12:05:29 +00:00
if slices . Contains ( p . GetOnlineClients ( ) , traffic . Email ) {
status = t . I18nBot ( "tgbot.online" )
isOnline = true
2024-01-01 15:07:56 +00:00
}
}
output := ""
output += t . I18nBot ( "tgbot.messages.email" , "Email==" + traffic . Email )
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>
2026-05-18 00:11:30 +00:00
if attachIds , err := t . clientService . GetInboundIdsForEmail ( nil , traffic . Email ) ; err == nil && len ( attachIds ) > 0 {
output += fmt . Sprintf ( "🔗 Inbounds: %s\r\n" , t . describeAttachedInbounds ( attachIds ) )
}
2024-01-01 15:07:56 +00:00
if printEnabled {
output += t . I18nBot ( "tgbot.messages.enabled" , "Enable==" + enabled )
}
if printOnline {
output += t . I18nBot ( "tgbot.messages.online" , "Status==" + status )
2025-12-03 13:43:37 +00:00
if ! isOnline && traffic . LastOnline > 0 {
output += t . I18nBot ( "tgbot.messages.lastOnline" , "Time==" + time . UnixMilli ( traffic . LastOnline ) . Format ( "2006-01-02 15:04:05" ) )
}
2024-01-01 15:07:56 +00:00
}
if printActive {
output += t . I18nBot ( "tgbot.messages.active" , "Enable==" + active )
}
if printDate {
if flag {
output += t . I18nBot ( "tgbot.messages.expireIn" , "Time==" + expiryTime )
} else {
output += t . I18nBot ( "tgbot.messages.expire" , "Time==" + expiryTime )
2023-05-05 23:06:46 +00:00
}
2023-05-05 14:50:56 +00:00
}
2024-01-01 15:07:56 +00:00
if printTraffic {
output += t . I18nBot ( "tgbot.messages.upload" , "Upload==" + common . FormatTraffic ( traffic . Up ) )
output += t . I18nBot ( "tgbot.messages.download" , "Download==" + common . FormatTraffic ( traffic . Down ) )
output += t . I18nBot ( "tgbot.messages.total" , "UpDown==" + common . FormatTraffic ( ( traffic . Up + traffic . Down ) ) , "Total==" + total )
}
if printRefreshed {
output += t . I18nBot ( "tgbot.messages.refreshedOn" , "Time==" + time . Now ( ) . Format ( "2006-01-02 15:04:05" ) )
}
return output
}
2025-09-20 07:35:50 +00:00
// getClientUsage retrieves and sends client usage information to the chat.
2024-04-02 11:34:44 +00:00
func ( t * Tgbot ) getClientUsage ( chatId int64 , tgUserID int64 , email ... string ) {
2024-01-01 15:07:56 +00:00
traffics , err := t . inboundService . GetClientTrafficTgBot ( tgUserID )
2023-03-17 16:07:49 +00:00
if err != nil {
logger . Warning ( err )
2023-05-20 23:00:26 +00:00
msg := t . I18nBot ( "tgbot.wentWrong" )
2023-03-17 16:07:49 +00:00
t . SendMsgToTgbot ( chatId , msg )
return
}
2024-01-01 15:07:56 +00:00
2023-03-17 16:07:49 +00:00
if len ( traffics ) == 0 {
2024-04-02 11:34:44 +00:00
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.answers.askToAddUserId" , "TgUserID==" + strconv . FormatInt ( tgUserID , 10 ) ) )
2023-04-09 19:43:18 +00:00
return
2023-03-17 16:07:49 +00:00
}
2023-05-20 23:00:26 +00:00
2024-01-01 15:07:56 +00:00
output := ""
2023-05-20 23:00:26 +00:00
2024-01-01 15:07:56 +00:00
if len ( traffics ) > 0 {
if len ( email ) > 0 {
for _ , traffic := range traffics {
if traffic . Email == email [ 0 ] {
output := t . clientInfoMsg ( traffic , true , true , true , true , true , true )
t . SendMsgToTgbot ( chatId , output )
return
}
2023-05-30 11:37:23 +00:00
}
2024-01-01 15:07:56 +00:00
msg := t . I18nBot ( "tgbot.noResult" )
t . SendMsgToTgbot ( chatId , msg )
return
2023-05-30 11:37:23 +00:00
} else {
2024-01-01 15:07:56 +00:00
for _ , traffic := range traffics {
output += t . clientInfoMsg ( traffic , true , true , true , true , true , false )
output += "\r\n"
}
2023-05-30 11:37:23 +00:00
}
2023-03-17 16:07:49 +00:00
}
2024-01-01 15:07:56 +00:00
output += t . I18nBot ( "tgbot.messages.refreshedOn" , "Time==" + time . Now ( ) . Format ( "2006-01-02 15:04:05" ) )
t . SendMsgToTgbot ( chatId , output )
output = t . I18nBot ( "tgbot.commands.pleaseChoose" )
t . SendAnswer ( chatId , output , false )
2023-03-17 16:07:49 +00:00
}
2025-09-20 07:35:50 +00:00
// searchClientIps searches and sends client IP addresses for the given email.
2023-05-05 14:50:56 +00:00
func ( t * Tgbot ) searchClientIps ( chatId int64 , email string , messageID ... int ) {
ips , err := t . inboundService . GetInboundClientIps ( email )
if err != nil || len ( ips ) == 0 {
2023-05-20 23:00:26 +00:00
ips = t . I18nBot ( "tgbot.noIpRecord" )
2023-05-05 14:50:56 +00:00
}
2023-05-20 23:00:26 +00:00
2026-02-11 21:21:09 +00:00
formattedIps := ips
if err == nil && len ( ips ) > 0 {
type ipWithTimestamp struct {
IP string ` json:"ip" `
Timestamp int64 ` json:"timestamp" `
}
var ipsWithTime [ ] ipWithTimestamp
if json . Unmarshal ( [ ] byte ( ips ) , & ipsWithTime ) == nil && len ( ipsWithTime ) > 0 {
lines := make ( [ ] string , 0 , len ( ipsWithTime ) )
for _ , item := range ipsWithTime {
if item . IP == "" {
continue
}
if item . Timestamp > 0 {
ts := time . Unix ( item . Timestamp , 0 ) . Format ( "2006-01-02 15:04:05" )
lines = append ( lines , fmt . Sprintf ( "%s (%s)" , item . IP , ts ) )
continue
}
lines = append ( lines , item . IP )
}
if len ( lines ) > 0 {
formattedIps = strings . Join ( lines , "\n" )
}
} else {
var oldIps [ ] string
if json . Unmarshal ( [ ] byte ( ips ) , & oldIps ) == nil && len ( oldIps ) > 0 {
formattedIps = strings . Join ( oldIps , "\n" )
}
}
}
2023-05-20 23:00:26 +00:00
output := ""
output += t . I18nBot ( "tgbot.messages.email" , "Email==" + email )
2026-02-11 21:21:09 +00:00
output += t . I18nBot ( "tgbot.messages.ips" , "IPs==" + formattedIps )
2023-11-20 14:17:59 +00:00
output += t . I18nBot ( "tgbot.messages.refreshedOn" , "Time==" + time . Now ( ) . Format ( "2006-01-02 15:04:05" ) )
2023-05-20 23:00:26 +00:00
2023-05-14 15:20:01 +00:00
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.refresh" ) ) . WithCallbackData ( t . encodeQuery ( "ips_refresh " + email ) ) ,
2023-05-05 14:50:56 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.clearIPs" ) ) . WithCallbackData ( t . encodeQuery ( "clear_ips " + email ) ) ,
2023-05-05 14:50:56 +00:00
) ,
)
2023-05-20 23:00:26 +00:00
2023-05-05 14:50:56 +00:00
if len ( messageID ) > 0 {
t . editMessageTgBot ( chatId , messageID [ 0 ] , output , inlineKeyboard )
} else {
t . SendMsgToTgbot ( chatId , output , inlineKeyboard )
}
}
2025-09-20 07:35:50 +00:00
// clientTelegramUserInfo retrieves and sends Telegram user info for the client.
2023-05-14 19:13:23 +00:00
func ( t * Tgbot ) clientTelegramUserInfo ( chatId int64 , email string , messageID ... int ) {
2023-05-14 18:37:49 +00:00
traffic , client , err := t . inboundService . GetClientByEmail ( email )
if err != nil {
logger . Warning ( err )
2023-05-20 23:00:26 +00:00
msg := t . I18nBot ( "tgbot.wentWrong" )
2023-05-14 18:37:49 +00:00
t . SendMsgToTgbot ( chatId , msg )
return
}
if client == nil {
2023-05-20 23:00:26 +00:00
msg := t . I18nBot ( "tgbot.noResult" )
2023-05-14 18:37:49 +00:00
t . SendMsgToTgbot ( chatId , msg )
return
}
2023-05-20 15:09:01 +00:00
tgId := "None"
2024-04-02 11:34:44 +00:00
if client . TgID != 0 {
tgId = strconv . FormatInt ( client . TgID , 10 )
2023-05-14 18:37:49 +00:00
}
2023-05-20 15:09:01 +00:00
2023-05-20 23:00:26 +00:00
output := ""
output += t . I18nBot ( "tgbot.messages.email" , "Email==" + email )
output += t . I18nBot ( "tgbot.messages.TGUser" , "TelegramID==" + tgId )
2023-05-30 22:01:00 +00:00
output += t . I18nBot ( "tgbot.messages.refreshedOn" , "Time==" + time . Now ( ) . Format ( "2006-01-02 15:04:05" ) )
2023-05-20 23:00:26 +00:00
2023-05-14 19:13:23 +00:00
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.refresh" ) ) . WithCallbackData ( t . encodeQuery ( "tgid_refresh " + email ) ) ,
2023-05-14 18:37:49 +00:00
) ,
2023-05-14 19:13:23 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.removeTGUser" ) ) . WithCallbackData ( t . encodeQuery ( "tgid_remove " + email ) ) ,
2023-05-14 18:37:49 +00:00
) ,
2023-05-14 19:13:23 +00:00
)
2023-05-20 15:09:01 +00:00
2023-05-14 19:13:23 +00:00
if len ( messageID ) > 0 {
t . editMessageTgBot ( chatId , messageID [ 0 ] , output , inlineKeyboard )
} else {
t . SendMsgToTgbot ( chatId , output , inlineKeyboard )
2024-02-17 17:45:53 +00:00
requestUser := telego . KeyboardButtonRequestUsers {
2023-05-14 19:13:23 +00:00
RequestID : int32 ( traffic . Id ) ,
2023-08-02 13:33:59 +00:00
UserIsBot : new ( bool ) ,
2023-05-14 19:13:23 +00:00
}
keyboard := tu . Keyboard (
tu . KeyboardRow (
2024-02-17 17:45:53 +00:00
tu . KeyboardButton ( t . I18nBot ( "tgbot.buttons.selectTGUser" ) ) . WithRequestUsers ( & requestUser ) ,
2023-05-14 19:13:23 +00:00
) ,
tu . KeyboardRow (
2023-05-20 23:00:26 +00:00
tu . KeyboardButton ( t . I18nBot ( "tgbot.buttons.closeKeyboard" ) ) ,
2023-05-14 19:13:23 +00:00
) ,
2023-05-14 19:25:01 +00:00
) . WithIsPersistent ( ) . WithResizeKeyboard ( )
2023-05-20 23:00:26 +00:00
t . SendMsgToTgbot ( chatId , t . I18nBot ( "tgbot.buttons.selectOneTGUser" ) , keyboard )
2023-05-14 19:13:23 +00:00
}
2023-05-14 18:37:49 +00:00
}
2025-09-20 07:35:50 +00:00
// searchClient searches for a client by email and sends the information.
2023-05-04 21:46:43 +00:00
func ( t * Tgbot ) searchClient ( chatId int64 , email string , messageID ... int ) {
2023-04-25 11:08:35 +00:00
traffic , err := t . inboundService . GetClientTrafficByEmail ( email )
2023-03-17 16:07:49 +00:00
if err != nil {
logger . Warning ( err )
2023-05-20 23:00:26 +00:00
msg := t . I18nBot ( "tgbot.wentWrong" )
2023-03-17 16:07:49 +00:00
t . SendMsgToTgbot ( chatId , msg )
return
}
2023-04-25 11:08:35 +00:00
if traffic == nil {
2023-05-20 23:00:26 +00:00
msg := t . I18nBot ( "tgbot.noResult" )
2023-03-17 16:07:49 +00:00
t . SendMsgToTgbot ( chatId , msg )
return
}
2023-05-20 15:09:01 +00:00
2024-01-01 15:07:56 +00:00
output := t . clientInfoMsg ( traffic , true , true , true , true , true , true )
2023-05-20 15:09:01 +00:00
2023-05-14 15:20:01 +00:00
inlineKeyboard := tu . InlineKeyboard (
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.refresh" ) ) . WithCallbackData ( t . encodeQuery ( "client_refresh " + email ) ) ,
2023-05-04 22:18:37 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.resetTraffic" ) ) . WithCallbackData ( t . encodeQuery ( "reset_traffic " + email ) ) ,
2023-11-20 14:17:59 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.limitTraffic" ) ) . WithCallbackData ( t . encodeQuery ( "limit_traffic " + email ) ) ,
2023-05-04 21:46:43 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.resetExpire" ) ) . WithCallbackData ( t . encodeQuery ( "reset_exp " + email ) ) ,
2023-05-04 21:46:43 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.ipLog" ) ) . WithCallbackData ( t . encodeQuery ( "ip_log " + email ) ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.ipLimit" ) ) . WithCallbackData ( t . encodeQuery ( "ip_limit " + email ) ) ,
2023-05-05 14:50:56 +00:00
) ,
2023-05-14 18:37:49 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.setTGUser" ) ) . WithCallbackData ( t . encodeQuery ( "tg_user " + email ) ) ,
2023-05-14 18:37:49 +00:00
) ,
2023-05-14 15:20:01 +00:00
tu . InlineKeyboardRow (
2023-05-21 04:03:08 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.toggle" ) ) . WithCallbackData ( t . encodeQuery ( "toggle_enable " + email ) ) ,
2023-05-05 14:50:56 +00:00
) ,
2023-05-04 21:46:43 +00:00
)
if len ( messageID ) > 0 {
t . editMessageTgBot ( chatId , messageID [ 0 ] , output , inlineKeyboard )
} else {
t . SendMsgToTgbot ( chatId , output , inlineKeyboard )
}
2023-03-17 16:07:49 +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>
2026-05-18 00:11:30 +00:00
// 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.
2026-03-17 21:09:49 +00:00
func ( t * Tgbot ) getCommonClientButtons ( ) [ ] [ ] telego . InlineKeyboardButton {
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>
2026-05-18 00:11:30 +00:00
attachLabel := fmt . Sprintf ( "➕ Attach inbound (%d)" , len ( receiver_inbound_IDs ) )
2026-03-17 21:09:49 +00:00
return [ ] [ ] telego . InlineKeyboardButton {
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>
2026-05-18 00:11:30 +00:00
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" ) ,
) ,
2026-03-17 21:09:49 +00:00
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.ipLimit" ) ) . WithCallbackData ( "add_client_ch_default_ip_limit" ) ,
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>
2026-05-18 00:11:30 +00:00
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.setTGUser" ) ) . WithCallbackData ( "add_client_ch_default_tg_id" ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( attachLabel ) . WithCallbackData ( "add_client_attach_more" ) ,
2026-03-17 21:09:49 +00:00
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.submitDisable" ) ) . WithCallbackData ( "add_client_submit_disable" ) ,
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.submitEnable" ) ) . WithCallbackData ( "add_client_submit_enable" ) ,
) ,
tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.cancel" ) ) . WithCallbackData ( "add_client_cancel" ) ,
) ,
}
}
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>
2026-05-18 00:11:30 +00:00
// addClient renders the draft message + shared client-first keyboard.
2025-03-26 18:16:35 +00:00
func ( t * Tgbot ) addClient ( chatId int64 , msg string , messageID ... int ) {
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>
2026-05-18 00:11:30 +00:00
inlineKeyboard := tu . InlineKeyboard ( t . getCommonClientButtons ( ) ... )
2026-03-17 21:09:49 +00:00
if len ( messageID ) > 0 {
t . editMessageTgBot ( chatId , messageID [ 0 ] , msg , inlineKeyboard )
} else {
t . SendMsgToTgbot ( chatId , msg , inlineKeyboard )
}
2025-03-26 18:16:35 +00:00
}
2025-09-20 07:35:50 +00:00
// searchInbound searches for inbounds by remark and sends the results.
2023-03-24 13:44:26 +00:00
func ( t * Tgbot ) searchInbound ( chatId int64 , remark string ) {
2024-07-07 09:55:59 +00:00
inbounds , err := t . inboundService . SearchInbounds ( remark )
2023-03-24 13:44:26 +00:00
if err != nil {
logger . Warning ( err )
2023-05-20 23:00:26 +00:00
msg := t . I18nBot ( "tgbot.wentWrong" )
2023-03-24 13:44:26 +00:00
t . SendMsgToTgbot ( chatId , msg )
return
}
2024-07-07 09:55:59 +00:00
if len ( inbounds ) == 0 {
2023-05-20 23:00:26 +00:00
msg := t . I18nBot ( "tgbot.noInbounds" )
2023-05-20 15:09:01 +00:00
t . SendMsgToTgbot ( chatId , msg )
return
}
2023-05-30 22:01:00 +00:00
2024-07-07 09:55:59 +00:00
for _ , inbound := range inbounds {
2023-03-24 13:44:26 +00:00
info := ""
2023-05-20 23:00:26 +00:00
info += t . I18nBot ( "tgbot.messages.inbound" , "Remark==" + inbound . Remark )
info += t . I18nBot ( "tgbot.messages.port" , "Port==" + strconv . Itoa ( inbound . Port ) )
info += t . I18nBot ( "tgbot.messages.traffic" , "Total==" + common . FormatTraffic ( ( inbound . Up + inbound . Down ) ) , "Upload==" + common . FormatTraffic ( inbound . Up ) , "Download==" + common . FormatTraffic ( inbound . Down ) )
2023-03-24 13:44:26 +00:00
if inbound . ExpiryTime == 0 {
2023-05-31 08:37:03 +00:00
info += t . I18nBot ( "tgbot.messages.expire" , "Time==" + t . I18nBot ( "tgbot.unlimited" ) )
2023-03-24 13:44:26 +00:00
} else {
2023-05-31 08:37:03 +00:00
info += t . I18nBot ( "tgbot.messages.expire" , "Time==" + time . Unix ( ( inbound . ExpiryTime / 1000 ) , 0 ) . Format ( "2006-01-02 15:04:05" ) )
2023-03-24 13:44:26 +00:00
}
t . SendMsgToTgbot ( chatId , info )
2023-05-20 15:09:01 +00:00
2024-01-01 15:07:56 +00:00
if len ( inbound . ClientStats ) > 0 {
2026-03-04 12:05:29 +00:00
var output strings . Builder
2024-01-01 15:07:56 +00:00
for _ , traffic := range inbound . ClientStats {
2026-03-04 12:05:29 +00:00
output . WriteString ( t . clientInfoMsg ( & traffic , true , true , true , true , true , true ) )
2023-05-30 11:37:23 +00:00
}
2026-03-04 12:05:29 +00:00
t . SendMsgToTgbot ( chatId , output . String ( ) )
2023-03-24 13:44:26 +00:00
}
}
}
2025-09-20 07:35:50 +00:00
// getExhausted retrieves and sends information about exhausted clients.
2024-01-01 15:07:56 +00:00
func ( t * Tgbot ) getExhausted ( chatId int64 ) {
2023-03-17 16:07:49 +00:00
trDiff := int64 ( 0 )
exDiff := int64 ( 0 )
now := time . Now ( ) . Unix ( ) * 1000
var exhaustedInbounds [ ] model . Inbound
var exhaustedClients [ ] xray . ClientTraffic
var disabledInbounds [ ] model . Inbound
var disabledClients [ ] xray . ClientTraffic
2023-05-20 15:09:01 +00:00
2023-04-09 19:43:18 +00:00
TrafficThreshold , err := t . settingService . GetTrafficDiff ( )
2023-03-17 16:07:49 +00:00
if err == nil && TrafficThreshold > 0 {
trDiff = int64 ( TrafficThreshold ) * 1073741824
}
2023-04-09 19:43:18 +00:00
ExpireThreshold , err := t . settingService . GetExpireDiff ( )
2023-03-17 16:07:49 +00:00
if err == nil && ExpireThreshold > 0 {
2023-04-09 19:43:18 +00:00
exDiff = int64 ( ExpireThreshold ) * 86400000
2023-03-17 16:07:49 +00:00
}
inbounds , err := t . inboundService . GetAllInbounds ( )
if err != nil {
logger . Warning ( "Unable to load Inbounds" , err )
}
2023-05-20 15:09:01 +00:00
2023-03-17 16:07:49 +00:00
for _ , inbound := range inbounds {
if inbound . Enable {
2023-03-24 13:20:10 +00:00
if ( inbound . ExpiryTime > 0 && ( inbound . ExpiryTime - now < exDiff ) ) ||
2023-04-09 21:25:47 +00:00
( inbound . Total > 0 && ( inbound . Total - ( inbound . Up + inbound . Down ) < trDiff ) ) {
2023-03-17 16:07:49 +00:00
exhaustedInbounds = append ( exhaustedInbounds , * inbound )
}
if len ( inbound . ClientStats ) > 0 {
for _ , client := range inbound . ClientStats {
if client . Enable {
2023-03-24 13:20:10 +00:00
if ( client . ExpiryTime > 0 && ( client . ExpiryTime - now < exDiff ) ) ||
2023-04-09 21:25:47 +00:00
( client . Total > 0 && ( client . Total - ( client . Up + client . Down ) < trDiff ) ) {
2023-03-17 16:07:49 +00:00
exhaustedClients = append ( exhaustedClients , client )
}
} else {
disabledClients = append ( disabledClients , client )
}
}
}
} else {
disabledInbounds = append ( disabledInbounds , * inbound )
}
}
2023-05-20 15:09:01 +00:00
2023-05-20 23:00:26 +00:00
// Inbounds
output := ""
output += t . I18nBot ( "tgbot.messages.exhaustedCount" , "Type==" + t . I18nBot ( "tgbot.inbounds" ) )
output += t . I18nBot ( "tgbot.messages.disabled" , "Disabled==" + strconv . Itoa ( len ( disabledInbounds ) ) )
output += t . I18nBot ( "tgbot.messages.depleteSoon" , "Deplete==" + strconv . Itoa ( len ( exhaustedInbounds ) ) )
2023-03-24 13:20:10 +00:00
if len ( exhaustedInbounds ) > 0 {
2024-01-01 15:07:56 +00:00
output += t . I18nBot ( "tgbot.messages.depleteSoon" , "Deplete==" + t . I18nBot ( "tgbot.inbounds" ) )
2023-05-20 23:00:26 +00:00
2023-03-17 16:07:49 +00:00
for _ , inbound := range exhaustedInbounds {
2023-05-20 23:00:26 +00:00
output += t . I18nBot ( "tgbot.messages.inbound" , "Remark==" + inbound . Remark )
output += t . I18nBot ( "tgbot.messages.port" , "Port==" + strconv . Itoa ( inbound . Port ) )
output += t . I18nBot ( "tgbot.messages.traffic" , "Total==" + common . FormatTraffic ( ( inbound . Up + inbound . Down ) ) , "Upload==" + common . FormatTraffic ( inbound . Up ) , "Download==" + common . FormatTraffic ( inbound . Down ) )
2023-03-17 16:07:49 +00:00
if inbound . ExpiryTime == 0 {
2023-05-31 08:37:03 +00:00
output += t . I18nBot ( "tgbot.messages.expire" , "Time==" + t . I18nBot ( "tgbot.unlimited" ) )
2023-03-17 16:07:49 +00:00
} else {
2023-05-31 08:37:03 +00:00
output += t . I18nBot ( "tgbot.messages.expire" , "Time==" + time . Unix ( ( inbound . ExpiryTime / 1000 ) , 0 ) . Format ( "2006-01-02 15:04:05" ) )
2023-03-17 16:07:49 +00:00
}
2024-01-01 15:07:56 +00:00
output += "\r\n"
2023-03-17 16:07:49 +00:00
}
}
2023-05-20 15:09:01 +00:00
2023-05-20 23:00:26 +00:00
// Clients
2024-01-01 15:07:56 +00:00
exhaustedCC := len ( exhaustedClients )
2023-05-20 23:00:26 +00:00
output += t . I18nBot ( "tgbot.messages.exhaustedCount" , "Type==" + t . I18nBot ( "tgbot.clients" ) )
output += t . I18nBot ( "tgbot.messages.disabled" , "Disabled==" + strconv . Itoa ( len ( disabledClients ) ) )
2024-01-01 15:07:56 +00:00
output += t . I18nBot ( "tgbot.messages.depleteSoon" , "Deplete==" + strconv . Itoa ( exhaustedCC ) )
2023-05-20 23:00:26 +00:00
2024-01-01 15:07:56 +00:00
if exhaustedCC > 0 {
output += t . I18nBot ( "tgbot.messages.depleteSoon" , "Deplete==" + t . I18nBot ( "tgbot.clients" ) )
var buttons [ ] telego . InlineKeyboardButton
2023-03-17 16:07:49 +00:00
for _ , traffic := range exhaustedClients {
2024-01-01 15:07:56 +00:00
output += t . clientInfoMsg ( & traffic , true , false , false , true , true , false )
output += "\r\n"
buttons = append ( buttons , tu . InlineKeyboardButton ( traffic . Email ) . WithCallbackData ( t . encodeQuery ( "client_get_usage " + traffic . Email ) ) )
}
cols := 0
if exhaustedCC < 11 {
cols = 1
} else {
cols = 2
}
output += t . I18nBot ( "tgbot.messages.refreshedOn" , "Time==" + time . Now ( ) . Format ( "2006-01-02 15:04:05" ) )
keyboard := tu . InlineKeyboardGrid ( tu . InlineKeyboardCols ( cols , buttons ... ) )
t . SendMsgToTgbot ( chatId , output , keyboard )
} else {
output += t . I18nBot ( "tgbot.messages.refreshedOn" , "Time==" + time . Now ( ) . Format ( "2006-01-02 15:04:05" ) )
t . SendMsgToTgbot ( chatId , output )
}
}
2023-05-20 23:00:26 +00:00
2025-09-20 07:35:50 +00:00
// notifyExhausted sends notifications for exhausted clients.
2024-01-01 15:07:56 +00:00
func ( t * Tgbot ) notifyExhausted ( ) {
trDiff := int64 ( 0 )
exDiff := int64 ( 0 )
now := time . Now ( ) . Unix ( ) * 1000
2023-05-20 23:00:26 +00:00
2024-01-01 15:07:56 +00:00
TrafficThreshold , err := t . settingService . GetTrafficDiff ( )
if err == nil && TrafficThreshold > 0 {
trDiff = int64 ( TrafficThreshold ) * 1073741824
}
ExpireThreshold , err := t . settingService . GetExpireDiff ( )
if err == nil && ExpireThreshold > 0 {
exDiff = int64 ( ExpireThreshold ) * 86400000
}
inbounds , err := t . inboundService . GetAllInbounds ( )
if err != nil {
logger . Warning ( "Unable to load Inbounds" , err )
}
2024-04-02 11:34:44 +00:00
var chatIDsDone [ ] int64
2024-01-01 15:07:56 +00:00
for _ , inbound := range inbounds {
if inbound . Enable {
if len ( inbound . ClientStats ) > 0 {
clients , err := t . inboundService . GetClients ( inbound )
if err == nil {
for _ , client := range clients {
2024-04-02 11:34:44 +00:00
if client . TgID != 0 {
chatID := client . TgID
if ! int64Contains ( chatIDsDone , chatID ) && ! checkAdmin ( chatID ) {
2024-01-01 15:07:56 +00:00
var disabledClients [ ] xray . ClientTraffic
var exhaustedClients [ ] xray . ClientTraffic
traffics , err := t . inboundService . GetClientTrafficTgBot ( client . TgID )
2024-04-02 11:34:44 +00:00
if err == nil && len ( traffics ) > 0 {
2024-01-01 15:07:56 +00:00
output := t . I18nBot ( "tgbot.messages.exhaustedCount" , "Type==" + t . I18nBot ( "tgbot.clients" ) )
for _ , traffic := range traffics {
if traffic . Enable {
if ( traffic . ExpiryTime > 0 && ( traffic . ExpiryTime - now < exDiff ) ) ||
( traffic . Total > 0 && ( traffic . Total - ( traffic . Up + traffic . Down ) < trDiff ) ) {
exhaustedClients = append ( exhaustedClients , * traffic )
}
} else {
disabledClients = append ( disabledClients , * traffic )
}
}
if len ( exhaustedClients ) > 0 {
output += t . I18nBot ( "tgbot.messages.disabled" , "Disabled==" + strconv . Itoa ( len ( disabledClients ) ) )
if len ( disabledClients ) > 0 {
output += t . I18nBot ( "tgbot.clients" ) + ":\r\n"
for _ , traffic := range disabledClients {
output += " " + traffic . Email
}
output += "\r\n"
}
output += "\r\n"
output += t . I18nBot ( "tgbot.messages.depleteSoon" , "Deplete==" + strconv . Itoa ( len ( exhaustedClients ) ) )
for _ , traffic := range exhaustedClients {
output += t . clientInfoMsg ( & traffic , true , false , false , true , true , false )
output += "\r\n"
}
t . SendMsgToTgbot ( chatID , output )
}
2024-04-02 11:34:44 +00:00
chatIDsDone = append ( chatIDsDone , chatID )
2024-01-01 15:07:56 +00:00
}
}
}
}
2023-05-30 11:37:23 +00:00
}
}
2023-03-17 16:07:49 +00:00
}
}
2024-01-01 15:07:56 +00:00
}
2023-03-17 16:07:49 +00:00
2025-09-20 07:35:50 +00:00
// int64Contains checks if an int64 slice contains a specific item.
2024-04-02 11:34:44 +00:00
func int64Contains ( slice [ ] int64 , item int64 ) bool {
2025-03-26 18:16:35 +00:00
for _ , s := range slice {
if s == item {
return true
}
}
return false
2024-04-02 11:34:44 +00:00
}
2025-09-20 07:35:50 +00:00
// onlineClients retrieves and sends information about online clients.
2024-01-01 15:07:56 +00:00
func ( t * Tgbot ) onlineClients ( chatId int64 , messageID ... int ) {
if ! p . IsRunning ( ) {
return
}
onlines := p . GetOnlineClients ( )
onlinesCount := len ( onlines )
output := t . I18nBot ( "tgbot.messages.onlinesCount" , "Count==" + fmt . Sprint ( onlinesCount ) )
keyboard := tu . InlineKeyboard ( tu . InlineKeyboardRow (
tu . InlineKeyboardButton ( t . I18nBot ( "tgbot.buttons.refresh" ) ) . WithCallbackData ( t . encodeQuery ( "onlines_refresh" ) ) ) )
if onlinesCount > 0 {
var buttons [ ] telego . InlineKeyboardButton
for _ , online := range onlines {
buttons = append ( buttons , tu . InlineKeyboardButton ( online ) . WithCallbackData ( t . encodeQuery ( "client_get_usage " + online ) ) )
}
cols := 0
if onlinesCount < 21 {
cols = 2
} else if onlinesCount < 61 {
cols = 3
} else {
cols = 4
}
keyboard . InlineKeyboard = append ( keyboard . InlineKeyboard , tu . InlineKeyboardCols ( cols , buttons ... ) ... )
}
if len ( messageID ) > 0 {
t . editMessageTgBot ( chatId , messageID [ 0 ] , output , keyboard )
} else {
t . SendMsgToTgbot ( chatId , output , keyboard )
}
2023-03-17 16:07:49 +00:00
}
2025-09-20 07:35:50 +00:00
// sendBackup sends a backup of the database and configuration files.
2023-03-17 16:07:49 +00:00
func ( t * Tgbot ) sendBackup ( chatId int64 ) {
2023-05-21 03:11:59 +00:00
output := t . I18nBot ( "tgbot.messages.backupTime" , "Time==" + time . Now ( ) . Format ( "2006-01-02 15:04:05" ) )
t . SendMsgToTgbot ( chatId , output )
2023-12-08 19:35:10 +00:00
// Update by manually trigger a checkpoint operation
err := database . Checkpoint ( )
if err != nil {
2024-01-01 15:07:56 +00:00
logger . Error ( "Error in trigger a checkpoint operation: " , err )
2023-12-08 19:35:10 +00:00
}
2026-02-14 21:31:41 +00:00
// Send database backup
2023-05-14 15:20:01 +00:00
file , err := os . Open ( config . GetDBPath ( ) )
2024-01-01 15:07:56 +00:00
if err == nil {
2026-02-14 21:31:41 +00:00
defer file . Close ( )
ctx , cancel := context . WithTimeout ( context . Background ( ) , 30 * time . Second )
defer cancel ( )
2024-01-01 15:07:56 +00:00
document := tu . Document (
tu . ID ( chatId ) ,
tu . File ( file ) ,
)
2026-02-14 21:31:41 +00:00
_ , err = bot . SendDocument ( ctx , document )
2024-01-01 15:07:56 +00:00
if err != nil {
logger . Error ( "Error in uploading backup: " , err )
}
} else {
logger . Error ( "Error in opening db file for backup: " , err )
2023-03-17 16:07:49 +00:00
}
2023-05-21 04:03:08 +00:00
2026-02-14 21:31:41 +00:00
// Small delay between file sends
time . Sleep ( 500 * time . Millisecond )
// Send config.json backup
2023-05-14 15:20:01 +00:00
file , err = os . Open ( xray . GetConfigPath ( ) )
2024-01-01 15:07:56 +00:00
if err == nil {
2026-02-14 21:31:41 +00:00
defer file . Close ( )
ctx , cancel := context . WithTimeout ( context . Background ( ) , 30 * time . Second )
defer cancel ( )
2024-01-01 15:07:56 +00:00
document := tu . Document (
tu . ID ( chatId ) ,
tu . File ( file ) ,
)
2026-02-14 21:31:41 +00:00
_ , err = bot . SendDocument ( ctx , document )
2024-01-01 15:07:56 +00:00
if err != nil {
logger . Error ( "Error in uploading config.json: " , err )
}
} else {
logger . Error ( "Error in opening config.json file for backup: " , err )
2023-05-14 15:20:01 +00:00
}
2024-01-01 15:07:56 +00:00
}
2025-09-20 07:35:50 +00:00
// sendBanLogs sends the ban logs to the specified chat.
2024-01-01 15:07:56 +00:00
func ( t * Tgbot ) sendBanLogs ( chatId int64 , dt bool ) {
if dt {
output := t . I18nBot ( "tgbot.messages.datetime" , "DateTime==" + time . Now ( ) . Format ( "2006-01-02 15:04:05" ) )
t . SendMsgToTgbot ( chatId , output )
}
file , err := os . Open ( xray . GetIPLimitBannedPrevLogPath ( ) )
if err == nil {
2024-02-03 22:20:14 +00:00
// Check if the file is non-empty before attempting to upload
fileInfo , _ := file . Stat ( )
if fileInfo . Size ( ) > 0 {
document := tu . Document (
tu . ID ( chatId ) ,
tu . File ( file ) ,
)
2025-08-08 18:41:06 +00:00
_ , err = bot . SendDocument ( context . Background ( ) , document )
2024-02-03 22:20:14 +00:00
if err != nil {
logger . Error ( "Error in uploading IPLimitBannedPrevLog: " , err )
}
} else {
logger . Warning ( "IPLimitBannedPrevLog file is empty, not uploading." )
2024-01-01 15:07:56 +00:00
}
2024-02-03 22:20:14 +00:00
file . Close ( )
2024-01-01 15:07:56 +00:00
} else {
2024-02-03 22:20:14 +00:00
logger . Error ( "Error in opening IPLimitBannedPrevLog file for backup: " , err )
2024-01-01 15:07:56 +00:00
}
file , err = os . Open ( xray . GetIPLimitBannedLogPath ( ) )
if err == nil {
2024-02-03 22:20:14 +00:00
// Check if the file is non-empty before attempting to upload
fileInfo , _ := file . Stat ( )
if fileInfo . Size ( ) > 0 {
document := tu . Document (
tu . ID ( chatId ) ,
tu . File ( file ) ,
)
2025-08-08 18:41:06 +00:00
_ , err = bot . SendDocument ( context . Background ( ) , document )
2024-02-03 22:20:14 +00:00
if err != nil {
logger . Error ( "Error in uploading IPLimitBannedLog: " , err )
}
} else {
logger . Warning ( "IPLimitBannedLog file is empty, not uploading." )
2024-01-01 15:07:56 +00:00
}
2024-02-03 22:20:14 +00:00
file . Close ( )
2024-01-01 15:07:56 +00:00
} else {
2024-02-03 22:20:14 +00:00
logger . Error ( "Error in opening IPLimitBannedLog file for backup: " , err )
2023-03-24 13:44:26 +00:00
}
2023-03-17 16:07:49 +00:00
}
2023-05-04 21:46:43 +00:00
2025-09-20 07:35:50 +00:00
// sendCallbackAnswerTgBot answers a callback query with a message.
2023-05-04 21:46:43 +00:00
func ( t * Tgbot ) sendCallbackAnswerTgBot ( id string , message string ) {
2023-05-14 15:20:01 +00:00
params := telego . AnswerCallbackQueryParams {
CallbackQueryID : id ,
Text : message ,
}
2025-08-08 18:41:06 +00:00
if err := bot . AnswerCallbackQuery ( context . Background ( ) , & params ) ; err != nil {
2023-05-04 21:46:43 +00:00
logger . Warning ( err )
}
}
2025-09-20 07:35:50 +00:00
// editMessageCallbackTgBot edits the reply markup of a message.
2023-05-14 15:20:01 +00:00
func ( t * Tgbot ) editMessageCallbackTgBot ( chatId int64 , messageID int , inlineKeyboard * telego . InlineKeyboardMarkup ) {
params := telego . EditMessageReplyMarkupParams {
ChatID : tu . ID ( chatId ) ,
MessageID : messageID ,
ReplyMarkup : inlineKeyboard ,
}
2025-08-08 18:41:06 +00:00
if _ , err := bot . EditMessageReplyMarkup ( context . Background ( ) , & params ) ; err != nil {
2023-05-04 21:46:43 +00:00
logger . Warning ( err )
}
}
2025-09-20 07:35:50 +00:00
// editMessageTgBot edits the text and reply markup of a message.
2023-05-14 15:20:01 +00:00
func ( t * Tgbot ) editMessageTgBot ( chatId int64 , messageID int , text string , inlineKeyboard ... * telego . InlineKeyboardMarkup ) {
params := telego . EditMessageTextParams {
ChatID : tu . ID ( chatId ) ,
MessageID : messageID ,
Text : text ,
ParseMode : "HTML" ,
}
2023-05-04 21:46:43 +00:00
if len ( inlineKeyboard ) > 0 {
2023-05-14 15:20:01 +00:00
params . ReplyMarkup = inlineKeyboard [ 0 ]
2023-05-04 21:46:43 +00:00
}
2025-08-08 18:41:06 +00:00
if _ , err := bot . EditMessageText ( context . Background ( ) , & params ) ; err != nil {
2023-05-04 21:46:43 +00:00
logger . Warning ( err )
}
}
2025-03-26 18:16:35 +00:00
2025-09-20 07:35:50 +00:00
// SendMsgToTgbotDeleteAfter sends a message and deletes it after a specified delay.
2025-03-26 18:16:35 +00:00
func ( t * Tgbot ) SendMsgToTgbotDeleteAfter ( chatId int64 , msg string , delayInSeconds int , replyMarkup ... telego . ReplyMarkup ) {
2025-04-06 22:45:52 +00:00
// Determine if replyMarkup was passed; otherwise, set it to nil
var replyMarkupParam telego . ReplyMarkup
if len ( replyMarkup ) > 0 {
replyMarkupParam = replyMarkup [ 0 ] // Use the first element
}
// Send the message
2025-08-08 18:41:06 +00:00
sentMsg , err := bot . SendMessage ( context . Background ( ) , & telego . SendMessageParams {
2025-04-06 22:45:52 +00:00
ChatID : tu . ID ( chatId ) ,
Text : msg ,
ReplyMarkup : replyMarkupParam , // Use the correct replyMarkup value
} )
if err != nil {
logger . Warning ( "Failed to send message:" , err )
return
}
// Delete the sent message after the specified number of seconds
go func ( ) {
time . Sleep ( time . Duration ( delayInSeconds ) * time . Second ) // Wait for the specified delay
t . deleteMessageTgBot ( chatId , sentMsg . MessageID ) // Delete the message
delete ( userStates , chatId )
} ( )
2025-03-26 18:16:35 +00:00
}
2025-09-20 07:35:50 +00:00
// deleteMessageTgBot deletes a message from the chat.
2025-03-26 18:16:35 +00:00
func ( t * Tgbot ) deleteMessageTgBot ( chatId int64 , messageID int ) {
2025-04-06 22:45:52 +00:00
params := telego . DeleteMessageParams {
ChatID : tu . ID ( chatId ) ,
MessageID : messageID ,
}
2025-08-08 18:41:06 +00:00
if err := bot . DeleteMessage ( context . Background ( ) , & params ) ; err != nil {
2025-04-06 22:45:52 +00:00
logger . Warning ( "Failed to delete message:" , err )
} else {
logger . Info ( "Message deleted successfully" )
}
2025-03-26 18:16:35 +00:00
}
2025-09-20 07:35:50 +00:00
// isSingleWord checks if the text contains only a single word.
2025-03-26 18:16:35 +00:00
func ( t * Tgbot ) isSingleWord ( text string ) bool {
text = strings . TrimSpace ( text )
re := regexp . MustCompile ( ` \s+ ` )
return re . MatchString ( text )
}