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
|
|
|
|
2025-03-26 18:16:35 +00:00
|
|
|
"github.com/google/uuid"
|
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
|
|
|
|
2025-03-26 18:16:35 +00:00
|
|
|
// clients data to adding new client
|
2025-04-06 22:45:52 +00:00
|
|
|
receiver_inbound_ID int
|
|
|
|
|
client_Id string
|
|
|
|
|
client_Flow string
|
|
|
|
|
client_Email string
|
|
|
|
|
client_LimitIP int
|
|
|
|
|
client_TotalGB int64
|
|
|
|
|
client_ExpiryTime int64
|
|
|
|
|
client_Enable bool
|
|
|
|
|
client_TgID string
|
|
|
|
|
client_SubID string
|
|
|
|
|
client_Comment string
|
|
|
|
|
client_Reset int
|
|
|
|
|
client_Security string
|
2025-03-26 18:16:35 +00:00
|
|
|
client_ShPassword string
|
|
|
|
|
client_TrPassword string
|
2025-04-06 22:45:52 +00:00
|
|
|
client_Method string
|
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
|
|
|
|
|
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, ¶ms)
|
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 {
|
|
|
|
|
case "awaiting_id":
|
|
|
|
|
if client_Id == strings.TrimSpace(message.Text) {
|
|
|
|
|
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
|
|
|
|
delete(userStates, message.Chat.ID)
|
|
|
|
|
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
|
|
|
|
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
|
|
|
|
t.addClient(message.Chat.ID, message_text)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-03-26 18:16:35 +00:00
|
|
|
|
2025-11-01 11:56:55 +00:00
|
|
|
client_Id = strings.TrimSpace(message.Text)
|
|
|
|
|
if t.isSingleWord(client_Id) {
|
|
|
|
|
userStates[message.Chat.ID] = "awaiting_id"
|
|
|
|
|
|
|
|
|
|
cancel_btn_markup := tu.InlineKeyboard(
|
|
|
|
|
tu.InlineKeyboardRow(
|
|
|
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
|
|
|
|
|
} else {
|
|
|
|
|
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_id"), 3, tu.ReplyKeyboardRemove())
|
|
|
|
|
delete(userStates, message.Chat.ID)
|
|
|
|
|
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
|
|
|
|
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
|
|
|
|
t.addClient(message.Chat.ID, message_text)
|
|
|
|
|
}
|
|
|
|
|
case "awaiting_password_tr":
|
|
|
|
|
if client_TrPassword == strings.TrimSpace(message.Text) {
|
|
|
|
|
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
|
|
|
|
delete(userStates, message.Chat.ID)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-03-26 18:16:35 +00:00
|
|
|
|
2025-11-01 11:56:55 +00:00
|
|
|
client_TrPassword = strings.TrimSpace(message.Text)
|
|
|
|
|
if t.isSingleWord(client_TrPassword) {
|
|
|
|
|
userStates[message.Chat.ID] = "awaiting_password_tr"
|
|
|
|
|
|
|
|
|
|
cancel_btn_markup := tu.InlineKeyboard(
|
|
|
|
|
tu.InlineKeyboardRow(
|
|
|
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
|
|
|
|
|
} else {
|
|
|
|
|
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
|
|
|
|
|
delete(userStates, message.Chat.ID)
|
|
|
|
|
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
|
|
|
|
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
|
|
|
|
t.addClient(message.Chat.ID, message_text)
|
|
|
|
|
}
|
|
|
|
|
case "awaiting_password_sh":
|
|
|
|
|
if client_ShPassword == strings.TrimSpace(message.Text) {
|
|
|
|
|
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
|
|
|
|
delete(userStates, message.Chat.ID)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-03-26 18:16:35 +00:00
|
|
|
|
2025-11-01 11:56:55 +00:00
|
|
|
client_ShPassword = strings.TrimSpace(message.Text)
|
|
|
|
|
if t.isSingleWord(client_ShPassword) {
|
|
|
|
|
userStates[message.Chat.ID] = "awaiting_password_sh"
|
|
|
|
|
|
|
|
|
|
cancel_btn_markup := tu.InlineKeyboard(
|
|
|
|
|
tu.InlineKeyboardRow(
|
|
|
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
|
|
|
|
|
} else {
|
|
|
|
|
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
|
|
|
|
|
delete(userStates, message.Chat.ID)
|
|
|
|
|
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
|
|
|
|
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
|
|
|
|
t.addClient(message.Chat.ID, message_text)
|
|
|
|
|
}
|
|
|
|
|
case "awaiting_email":
|
|
|
|
|
if client_Email == strings.TrimSpace(message.Text) {
|
|
|
|
|
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
|
|
|
|
delete(userStates, message.Chat.ID)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-03-26 18:16:35 +00:00
|
|
|
|
2025-11-01 11:56:55 +00:00
|
|
|
client_Email = strings.TrimSpace(message.Text)
|
|
|
|
|
if t.isSingleWord(client_Email) {
|
|
|
|
|
userStates[message.Chat.ID] = "awaiting_email"
|
|
|
|
|
|
|
|
|
|
cancel_btn_markup := tu.InlineKeyboard(
|
|
|
|
|
tu.InlineKeyboardRow(
|
|
|
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
|
|
|
|
|
} else {
|
|
|
|
|
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_email"), 3, tu.ReplyKeyboardRemove())
|
|
|
|
|
delete(userStates, message.Chat.ID)
|
|
|
|
|
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
|
|
|
|
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
|
|
|
|
t.addClient(message.Chat.ID, message_text)
|
|
|
|
|
}
|
|
|
|
|
case "awaiting_comment":
|
|
|
|
|
if client_Comment == strings.TrimSpace(message.Text) {
|
|
|
|
|
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
|
|
|
|
delete(userStates, message.Chat.ID)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-03-26 18:16:35 +00:00
|
|
|
|
2025-11-01 11:56:55 +00:00
|
|
|
client_Comment = strings.TrimSpace(message.Text)
|
|
|
|
|
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_comment"), 3, tu.ReplyKeyboardRemove())
|
2025-03-26 18:16:35 +00:00
|
|
|
delete(userStates, message.Chat.ID)
|
|
|
|
|
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
|
|
|
|
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
|
|
|
|
t.addClient(message.Chat.ID, message_text)
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
needRestart, err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userID)
|
|
|
|
|
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
|
|
|
// randomShadowSocksPassword generates a random password for Shadowsocks.
|
2025-03-26 18:16:35 +00:00
|
|
|
func (t *Tgbot) randomShadowSocksPassword() string {
|
|
|
|
|
array := make([]byte, 32)
|
|
|
|
|
_, err := rand.Read(array)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return t.randomLowerAndNum(32)
|
|
|
|
|
}
|
|
|
|
|
return base64.StdEncoding.EncodeToString(array)
|
|
|
|
|
}
|
|
|
|
|
|
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 {
|
2024-03-15 18:13:20 +00:00
|
|
|
needRestart, err := t.inboundService.ResetClientTrafficLimitByEmail(email, limitTraffic)
|
|
|
|
|
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()
|
|
|
|
|
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
2025-08-17 11:37:49 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
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
|
|
|
}
|
2024-03-15 18:13:20 +00:00
|
|
|
needRestart, err := t.inboundService.ResetClientExpiryTimeByEmail(email, date)
|
|
|
|
|
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()
|
|
|
|
|
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
2025-08-17 11:37:49 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
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 {
|
2024-03-15 18:13:20 +00:00
|
|
|
needRestart, err := t.inboundService.ResetClientIpLimitByEmail(email, count)
|
|
|
|
|
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()
|
|
|
|
|
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
2025-08-17 11:37:49 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-05-06 16:27:17 +00:00
|
|
|
|
2026-05-15 11:12:54 +00:00
|
|
|
t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
|
|
|
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
|
|
|
|
|
case "add_client_set_flow":
|
|
|
|
|
if dataArray[1] == "none" {
|
|
|
|
|
client_Flow = ""
|
|
|
|
|
} else {
|
|
|
|
|
client_Flow = dataArray[1]
|
|
|
|
|
}
|
|
|
|
|
messageId := callbackQuery.Message.GetMessageID()
|
|
|
|
|
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
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
|
|
|
|
|
}
|
2024-04-02 11:34:44 +00:00
|
|
|
needRestart, err := t.inboundService.SetClientTelegramUserID(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":
|
2024-03-15 18:13:20 +00:00
|
|
|
enabled, needRestart, err := t.inboundService.ToggleClientEnableByEmail(email)
|
|
|
|
|
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":
|
|
|
|
|
// assign default values to clients variables
|
2025-04-06 22:45:52 +00:00
|
|
|
client_Id = uuid.New().String()
|
2025-03-26 18:16:35 +00:00
|
|
|
client_Flow = ""
|
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
|
|
|
|
|
client_Security = "auto"
|
|
|
|
|
client_ShPassword = t.randomShadowSocksPassword()
|
|
|
|
|
client_TrPassword = t.randomLowerAndNum(10)
|
|
|
|
|
client_Method = ""
|
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
|
|
|
|
|
inbound, err := t.inboundService.GetInbound(inboundIdInt)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-04-06 22:45:52 +00:00
|
|
|
|
2025-03-26 18:16:35 +00:00
|
|
|
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
2025-08-17 11:37:49 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-04-06 22:45:52 +00:00
|
|
|
|
2025-08-08 18:41:06 +00:00
|
|
|
t.addClient(callbackQuery.Message.GetChat().ID, message_text)
|
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":
|
|
|
|
|
// assign default values to clients variables
|
2025-04-06 22:45:52 +00:00
|
|
|
client_Id = uuid.New().String()
|
2025-03-26 18:16:35 +00:00
|
|
|
client_Flow = ""
|
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
|
|
|
|
|
client_Security = "auto"
|
|
|
|
|
client_ShPassword = t.randomShadowSocksPassword()
|
|
|
|
|
client_TrPassword = t.randomLowerAndNum(10)
|
|
|
|
|
client_Method = ""
|
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)
|
|
|
|
|
case "add_client_ch_default_id":
|
|
|
|
|
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
|
|
|
|
userStates[chatId] = "awaiting_id"
|
|
|
|
|
cancel_btn_markup := tu.InlineKeyboard(
|
|
|
|
|
tu.InlineKeyboardRow(
|
|
|
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
prompt_message := t.I18nBot("tgbot.messages.id_prompt", "ClientId=="+client_Id)
|
|
|
|
|
t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)
|
|
|
|
|
case "add_client_ch_default_pass_tr":
|
|
|
|
|
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
|
|
|
|
userStates[chatId] = "awaiting_password_tr"
|
|
|
|
|
cancel_btn_markup := tu.InlineKeyboard(
|
|
|
|
|
tu.InlineKeyboardRow(
|
|
|
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
prompt_message := t.I18nBot("tgbot.messages.pass_prompt", "ClientPassword=="+client_TrPassword)
|
|
|
|
|
t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)
|
|
|
|
|
case "add_client_ch_default_pass_sh":
|
|
|
|
|
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
|
|
|
|
userStates[chatId] = "awaiting_password_sh"
|
|
|
|
|
cancel_btn_markup := tu.InlineKeyboard(
|
|
|
|
|
tu.InlineKeyboardRow(
|
|
|
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
prompt_message := t.I18nBot("tgbot.messages.pass_prompt", "ClientPassword=="+client_ShPassword)
|
|
|
|
|
t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)
|
|
|
|
|
case "add_client_ch_default_comment":
|
|
|
|
|
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
|
|
|
|
userStates[chatId] = "awaiting_comment"
|
|
|
|
|
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.comment_prompt", "ClientComment=="+client_Comment)
|
2025-04-06 22:45:52 +00:00
|
|
|
t.SendMsgToTgbot(chatId, prompt_message, 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)
|
2026-05-15 11:12:54 +00:00
|
|
|
case "add_client_ch_default_flow":
|
|
|
|
|
inlineKeyboard := tu.InlineKeyboard(
|
|
|
|
|
tu.InlineKeyboardRow(
|
|
|
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("add_client_default_traffic_exp")),
|
|
|
|
|
),
|
|
|
|
|
tu.InlineKeyboardRow(
|
|
|
|
|
tu.InlineKeyboardButton("None").WithCallbackData(t.encodeQuery("add_client_set_flow none")),
|
|
|
|
|
),
|
|
|
|
|
tu.InlineKeyboardRow(
|
|
|
|
|
tu.InlineKeyboardButton("xtls-rprx-vision").WithCallbackData(t.encodeQuery("add_client_set_flow xtls-rprx-vision")),
|
|
|
|
|
),
|
|
|
|
|
tu.InlineKeyboardRow(
|
|
|
|
|
tu.InlineKeyboardButton("xtls-rprx-vision-udp443").WithCallbackData(t.encodeQuery("add_client_set_flow xtls-rprx-vision-udp443")),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
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)
|
|
|
|
|
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
|
|
|
|
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
|
|
|
|
t.addClient(chatId, message_text)
|
|
|
|
|
case "add_client_cancel":
|
|
|
|
|
delete(userStates, chatId)
|
|
|
|
|
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
|
|
|
|
t.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot("tgbot.messages.cancel"), 3, tu.ReplyKeyboardRemove())
|
|
|
|
|
case "add_client_default_traffic_exp":
|
|
|
|
|
messageId := callbackQuery.Message.GetMessageID()
|
|
|
|
|
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
2025-08-17 11:37:49 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
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()
|
|
|
|
|
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
2025-08-17 11:37:49 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-05-06 16:27:17 +00:00
|
|
|
t.addClient(chatId, message_text, messageId)
|
|
|
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email))
|
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)
|
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)
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-20 07:35:50 +00:00
|
|
|
// BuildInboundClientDataMessage builds a message with client data for the given inbound and protocol.
|
2025-04-06 22:45:52 +00:00
|
|
|
func (t *Tgbot) BuildInboundClientDataMessage(inbound_remark string, protocol model.Protocol) (string, error) {
|
|
|
|
|
var message string
|
2025-03-26 18:16:35 +00:00
|
|
|
|
|
|
|
|
currentTime := time.Now()
|
|
|
|
|
timestampMillis := currentTime.UnixNano() / int64(time.Millisecond)
|
|
|
|
|
|
|
|
|
|
expiryTime := ""
|
|
|
|
|
diff := client_ExpiryTime/1000 - timestampMillis
|
|
|
|
|
if client_ExpiryTime == 0 {
|
|
|
|
|
expiryTime = t.I18nBot("tgbot.unlimited")
|
|
|
|
|
} else if diff > 172800 {
|
|
|
|
|
expiryTime = time.Unix((client_ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
|
|
|
|
} else if client_ExpiryTime < 0 {
|
|
|
|
|
expiryTime = fmt.Sprintf("%d %s", client_ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
|
|
|
|
|
} else {
|
|
|
|
|
expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
traffic_value := ""
|
|
|
|
|
if client_TotalGB == 0 {
|
|
|
|
|
traffic_value = "♾️ Unlimited(Reset)"
|
2025-04-06 22:45:52 +00:00
|
|
|
} else {
|
2025-03-26 18:16:35 +00:00
|
|
|
traffic_value = common.FormatTraffic(client_TotalGB)
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-06 16:27:17 +00:00
|
|
|
ip_limit := ""
|
|
|
|
|
if client_LimitIP == 0 {
|
|
|
|
|
ip_limit = "♾️ Unlimited(Reset)"
|
|
|
|
|
} else {
|
|
|
|
|
ip_limit = fmt.Sprint(client_LimitIP)
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-06 22:45:52 +00:00
|
|
|
switch protocol {
|
|
|
|
|
case model.VMESS, model.VLESS:
|
2025-05-06 16:27:17 +00:00
|
|
|
message = t.I18nBot("tgbot.messages.inbound_client_data_id", "InboundRemark=="+inbound_remark, "ClientId=="+client_Id, "ClientEmail=="+client_Email, "ClientTraffic=="+traffic_value, "ClientExp=="+expiryTime, "IpLimit=="+ip_limit, "ClientComment=="+client_Comment)
|
2025-04-06 22:45:52 +00:00
|
|
|
|
2025-03-26 18:16:35 +00:00
|
|
|
case model.Trojan:
|
2025-05-06 16:27:17 +00:00
|
|
|
message = t.I18nBot("tgbot.messages.inbound_client_data_pass", "InboundRemark=="+inbound_remark, "ClientPass=="+client_TrPassword, "ClientEmail=="+client_Email, "ClientTraffic=="+traffic_value, "ClientExp=="+expiryTime, "IpLimit=="+ip_limit, "ClientComment=="+client_Comment)
|
2025-03-26 18:16:35 +00:00
|
|
|
|
2025-04-06 22:45:52 +00:00
|
|
|
case model.Shadowsocks:
|
2025-05-06 16:27:17 +00:00
|
|
|
message = t.I18nBot("tgbot.messages.inbound_client_data_pass", "InboundRemark=="+inbound_remark, "ClientPass=="+client_ShPassword, "ClientEmail=="+client_Email, "ClientTraffic=="+traffic_value, "ClientExp=="+expiryTime, "IpLimit=="+ip_limit, "ClientComment=="+client_Comment)
|
2025-03-26 18:16:35 +00:00
|
|
|
|
2025-04-06 22:45:52 +00:00
|
|
|
default:
|
|
|
|
|
return "", errors.New("unknown protocol")
|
|
|
|
|
}
|
2025-03-26 18:16:35 +00:00
|
|
|
|
2025-04-06 22:45:52 +00:00
|
|
|
return message, nil
|
2025-03-26 18:16:35 +00:00
|
|
|
}
|
|
|
|
|
|
2025-09-20 07:35:50 +00:00
|
|
|
// BuildJSONForProtocol builds a JSON string for the given protocol with client data.
|
2025-03-26 18:16:35 +00:00
|
|
|
func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) {
|
2025-04-06 22:45:52 +00:00
|
|
|
var jsonString string
|
2025-03-26 18:16:35 +00:00
|
|
|
|
2025-04-06 22:45:52 +00:00
|
|
|
switch protocol {
|
|
|
|
|
case model.VMESS:
|
|
|
|
|
jsonString = fmt.Sprintf(`{
|
2025-03-26 18:16:35 +00:00
|
|
|
"clients": [{
|
|
|
|
|
"id": "%s",
|
|
|
|
|
"security": "%s",
|
|
|
|
|
"email": "%s",
|
|
|
|
|
"limitIp": %d,
|
|
|
|
|
"totalGB": %d,
|
|
|
|
|
"expiryTime": %d,
|
|
|
|
|
"enable": %t,
|
|
|
|
|
"tgId": "%s",
|
|
|
|
|
"subId": "%s",
|
|
|
|
|
"comment": "%s",
|
|
|
|
|
"reset": %d
|
|
|
|
|
}]
|
|
|
|
|
}`, client_Id, client_Security, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset)
|
|
|
|
|
|
2025-04-06 22:45:52 +00:00
|
|
|
case model.VLESS:
|
|
|
|
|
jsonString = fmt.Sprintf(`{
|
2025-03-26 18:16:35 +00:00
|
|
|
"clients": [{
|
|
|
|
|
"id": "%s",
|
|
|
|
|
"flow": "%s",
|
|
|
|
|
"email": "%s",
|
|
|
|
|
"limitIp": %d,
|
|
|
|
|
"totalGB": %d,
|
|
|
|
|
"expiryTime": %d,
|
|
|
|
|
"enable": %t,
|
|
|
|
|
"tgId": "%s",
|
|
|
|
|
"subId": "%s",
|
|
|
|
|
"comment": "%s",
|
|
|
|
|
"reset": %d
|
|
|
|
|
}]
|
|
|
|
|
}`, client_Id, client_Flow, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset)
|
|
|
|
|
|
2025-04-06 22:45:52 +00:00
|
|
|
case model.Trojan:
|
|
|
|
|
jsonString = fmt.Sprintf(`{
|
2025-03-26 18:16:35 +00:00
|
|
|
"clients": [{
|
|
|
|
|
"password": "%s",
|
|
|
|
|
"email": "%s",
|
|
|
|
|
"limitIp": %d,
|
|
|
|
|
"totalGB": %d,
|
|
|
|
|
"expiryTime": %d,
|
|
|
|
|
"enable": %t,
|
|
|
|
|
"tgId": "%s",
|
|
|
|
|
"subId": "%s",
|
|
|
|
|
"comment": "%s",
|
|
|
|
|
"reset": %d
|
|
|
|
|
}]
|
|
|
|
|
}`, client_TrPassword, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset)
|
|
|
|
|
|
2025-04-06 22:45:52 +00:00
|
|
|
case model.Shadowsocks:
|
|
|
|
|
jsonString = fmt.Sprintf(`{
|
2025-03-26 18:16:35 +00:00
|
|
|
"clients": [{
|
|
|
|
|
"method": "%s",
|
|
|
|
|
"password": "%s",
|
|
|
|
|
"email": "%s",
|
|
|
|
|
"limitIp": %d,
|
|
|
|
|
"totalGB": %d,
|
|
|
|
|
"expiryTime": %d,
|
|
|
|
|
"enable": %t,
|
|
|
|
|
"tgId": "%s",
|
|
|
|
|
"subId": "%s",
|
|
|
|
|
"comment": "%s",
|
|
|
|
|
"reset": %d
|
|
|
|
|
}]
|
|
|
|
|
}`, client_Method, client_ShPassword, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset)
|
|
|
|
|
|
2025-04-06 22:45:52 +00:00
|
|
|
default:
|
|
|
|
|
return "", errors.New("unknown protocol")
|
|
|
|
|
}
|
2025-03-26 18:16:35 +00:00
|
|
|
|
2025-04-06 22:45:52 +00:00
|
|
|
return jsonString, nil
|
2025-03-26 18:16:35 +00:00
|
|
|
}
|
|
|
|
|
|
2025-09-20 07:35:50 +00:00
|
|
|
// SubmitAddClient submits the client addition request to the inbound service.
|
2025-03-26 18:16:35 +00:00
|
|
|
func (t *Tgbot) SubmitAddClient() (bool, error) {
|
|
|
|
|
|
|
|
|
|
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Warning("getIboundClients run failed:", err)
|
|
|
|
|
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
|
|
|
|
|
|
|
|
jsonString, err := t.BuildJSONForProtocol(inbound.Protocol)
|
2025-08-17 11:37:49 +00:00
|
|
|
if err != nil {
|
|
|
|
|
logger.Warning("BuildJSONForProtocol run failed:", err)
|
|
|
|
|
return false, errors.New("failed to build JSON for protocol")
|
|
|
|
|
}
|
2025-03-26 18:16:35 +00:00
|
|
|
|
|
|
|
|
newInbound := &model.Inbound{
|
|
|
|
|
Id: receiver_inbound_ID,
|
2025-04-06 22:45:52 +00:00
|
|
|
Settings: jsonString,
|
2025-03-26 18:16:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return t.inboundService.AddInboundClient(newInbound)
|
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, ¶ms)
|
|
|
|
|
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
|
2023-03-17 16:07:49 +00:00
|
|
|
// get traffic
|
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"))
|
2023-03-17 16:07:49 +00:00
|
|
|
} else {
|
|
|
|
|
// NOTE:If there no any sessions here,need to notify here
|
|
|
|
|
// TODO:Sub-node push, automatic conversion format
|
2024-07-07 09:55:59 +00:00
|
|
|
for _, inbound := range inbounds {
|
2026-03-04 12:05:29 +00:00
|
|
|
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)))
|
2023-05-20 23:00:26 +00:00
|
|
|
|
2023-03-17 16:07:49 +00:00
|
|
|
if inbound.ExpiryTime == 0 {
|
2026-03-04 12:05:29 +00:00
|
|
|
info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited")))
|
2023-03-17 16:07:49 +00:00
|
|
|
} else {
|
2026-03-04 12:05:29 +00:00
|
|
|
info.WriteString(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
|
|
|
}
|
2026-03-04 12:05:29 +00:00
|
|
|
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"))
|
|
|
|
|
}
|
|
|
|
|
|
feat(socks): complete backend integration for SOCKS5 inbound
Wraps up the 'help wanted' backend items from the SOCKS5 scaffold PR
(#4452) so the dedicated socks inbound is a fully functional protocol
end-to-end, not just a model constant.
xray/api.go AddUser
-------------------
Live add-user via the gRPC HandlerService now handles 'socks' and
'http' as first-class protocols. Previously these fell through the
default branch and returned nil, so adding a new password-mode account
to a running socks/http inbound silently required a full xray restart.
- New 'socks' case constructs a proxy/socks.Account{Username, Password}
from the panel-side map keys 'user' and 'pass' (matching how
Inbound.SocksSettings.SocksAccount serialises in
frontend/src/models/inbound.js). Username is required, password is
optional so a no-pass account is still expressible if Xray ever
allows it on a specific build.
- New 'http' case mirrors the same shape via proxy/http.Account.
The dedicated HTTP inbound isn't surfaced standalone in the panel
UI yet, but the runtime API is symmetric with socks and several
follow-up plans (e.g. exposing HTTP as a separate inbound) become
one-line UI work instead of a backend refactor.
Both branches reuse the existing getRequiredUserString /
getOptionalUserString helpers, so a malformed userMap surfaces the
same typed error message as the vless / vmess paths above.
web/service/port_conflict.go
----------------------------
inboundTransports() now folds 'socks' into the same branch that already
handles 'mixed': settings.udp=true means the inbound holds both tcp and
udp on the listening port (socks5 UDP ASSOCIATE), settings.udp=false
keeps it tcp-only. Without this, a socks+udp inbound would silently be
classified as tcp-only and the validator would let a hysteria2 udp
inbound coexist with it on the same port — both processes would then
race for the udp socket at xray start, with one of them quietly failing.
The two protocols share the exact same settings JSON shape for this
field (it's the same proxy/socks server type under the hood), so the
sane thing is to merge the case clauses rather than copy/paste the
type-assertion. Comment updated to spell out why.
web/service/tgbot.go
--------------------
Add model.Socks to the excludedProtocols set in getInboundsAddClient
so the Telegram bot doesn't offer a dedicated SOCKS inbound when the
admin asks 'add a client to which inbound?'. SOCKS inbounds, like
Mixed/HTTP, don't produce a per-client subscription URL (see the
existing link-less branch in sub/subService.go::GetLink), so any
client attached via the bot would have no way to actually subscribe.
Added a header comment explaining the criterion so future protocols
fall into the right bucket without an audit.
Tests
-----
web/service/port_conflict_test.go gains four cases that pin the new
behaviour at the transport-bits level (TestInboundTransports):
- socks + udp=true -> tcp|udp (matches Mixed)
- socks + udp=false -> tcp only
- socks + missing settings -> tcp only
- socks + empty settings -> tcp only
…plus two end-to-end conflict checks that mirror the existing
shadowsocks/mixed coverage:
- TestCheckPortConflict_SocksUDPBlocksUDPNeighbour: a socks+udp
inbound on port N must clash with a hysteria2/udp on the same
port. Catches a regression where the Socks branch is dropped
from inboundTransports.
- TestCheckPortConflict_SocksTCPCoexistsWithUDPNeighbour: a
socks-tcp-only inbound must still let a hysteria2/udp neighbour
bind the same port. Mirrors the #4103 vless+hysteria2 coexistence
case.
Out-of-scope (still tracked in the PR description)
--------------------------------------------------
- Sub-link generation (sub/subService.go GetLink): SOCKS deliberately
stays link-less for the reasons documented in the previous commit;
no socks:// scheme is consistently understood across xray/v2ray
client ecosystems.
- Routing UI: routing rules in this fork already accept any inbound
tag, so SOCKS inbounds are routable as-is. A dedicated
'protocol == socks' helper in the routing rule editor is a UX
follow-up, not a correctness gap.
- Translations: protocol labels are rendered raw in this fork; no
per-locale label key exists for vmess/vless/mixed either, so adding
one only for socks would be inconsistent.
2026-05-25 15:05:20 +00:00
|
|
|
// Protocols listed here are skipped when the bot asks "which inbound do
|
|
|
|
|
// you want to add a client to?". They're inbounds that either don't
|
|
|
|
|
// have per-client subscription links (Mixed/HTTP/Socks/Tunnel) or
|
|
|
|
|
// don't have per-client credentials at all (WireGuard), so attaching
|
|
|
|
|
// a tg-managed client to them would produce something the user can't
|
|
|
|
|
// actually subscribe to.
|
2025-03-26 18:16:35 +00:00
|
|
|
excludedProtocols := map[model.Protocol]bool{
|
2025-09-09 11:57:40 +00:00
|
|
|
model.Tunnel: true,
|
|
|
|
|
model.Mixed: true,
|
feat(socks): complete backend integration for SOCKS5 inbound
Wraps up the 'help wanted' backend items from the SOCKS5 scaffold PR
(#4452) so the dedicated socks inbound is a fully functional protocol
end-to-end, not just a model constant.
xray/api.go AddUser
-------------------
Live add-user via the gRPC HandlerService now handles 'socks' and
'http' as first-class protocols. Previously these fell through the
default branch and returned nil, so adding a new password-mode account
to a running socks/http inbound silently required a full xray restart.
- New 'socks' case constructs a proxy/socks.Account{Username, Password}
from the panel-side map keys 'user' and 'pass' (matching how
Inbound.SocksSettings.SocksAccount serialises in
frontend/src/models/inbound.js). Username is required, password is
optional so a no-pass account is still expressible if Xray ever
allows it on a specific build.
- New 'http' case mirrors the same shape via proxy/http.Account.
The dedicated HTTP inbound isn't surfaced standalone in the panel
UI yet, but the runtime API is symmetric with socks and several
follow-up plans (e.g. exposing HTTP as a separate inbound) become
one-line UI work instead of a backend refactor.
Both branches reuse the existing getRequiredUserString /
getOptionalUserString helpers, so a malformed userMap surfaces the
same typed error message as the vless / vmess paths above.
web/service/port_conflict.go
----------------------------
inboundTransports() now folds 'socks' into the same branch that already
handles 'mixed': settings.udp=true means the inbound holds both tcp and
udp on the listening port (socks5 UDP ASSOCIATE), settings.udp=false
keeps it tcp-only. Without this, a socks+udp inbound would silently be
classified as tcp-only and the validator would let a hysteria2 udp
inbound coexist with it on the same port — both processes would then
race for the udp socket at xray start, with one of them quietly failing.
The two protocols share the exact same settings JSON shape for this
field (it's the same proxy/socks server type under the hood), so the
sane thing is to merge the case clauses rather than copy/paste the
type-assertion. Comment updated to spell out why.
web/service/tgbot.go
--------------------
Add model.Socks to the excludedProtocols set in getInboundsAddClient
so the Telegram bot doesn't offer a dedicated SOCKS inbound when the
admin asks 'add a client to which inbound?'. SOCKS inbounds, like
Mixed/HTTP, don't produce a per-client subscription URL (see the
existing link-less branch in sub/subService.go::GetLink), so any
client attached via the bot would have no way to actually subscribe.
Added a header comment explaining the criterion so future protocols
fall into the right bucket without an audit.
Tests
-----
web/service/port_conflict_test.go gains four cases that pin the new
behaviour at the transport-bits level (TestInboundTransports):
- socks + udp=true -> tcp|udp (matches Mixed)
- socks + udp=false -> tcp only
- socks + missing settings -> tcp only
- socks + empty settings -> tcp only
…plus two end-to-end conflict checks that mirror the existing
shadowsocks/mixed coverage:
- TestCheckPortConflict_SocksUDPBlocksUDPNeighbour: a socks+udp
inbound on port N must clash with a hysteria2/udp on the same
port. Catches a regression where the Socks branch is dropped
from inboundTransports.
- TestCheckPortConflict_SocksTCPCoexistsWithUDPNeighbour: a
socks-tcp-only inbound must still let a hysteria2/udp neighbour
bind the same port. Mirrors the #4103 vless+hysteria2 coexistence
case.
Out-of-scope (still tracked in the PR description)
--------------------------------------------------
- Sub-link generation (sub/subService.go GetLink): SOCKS deliberately
stays link-less for the reasons documented in the previous commit;
no socks:// scheme is consistently understood across xray/v2ray
client ecosystems.
- Routing UI: routing rules in this fork already accept any inbound
tag, so SOCKS inbounds are routable as-is. A dedicated
'protocol == socks' helper in the routing rule editor is a UX
follow-up, not a correctness gap.
- Translations: protocol labels are rendered raw in this fork; no
per-locale label key exists for vmess/vless/mixed either, so adding
one only for socks would be inconsistent.
2026-05-25 15:05:20 +00:00
|
|
|
model.Socks: 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
|
|
|
|
|
}
|
|
|
|
|
|
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 := ""
|
|
|
|
|
isEnabled, err := t.inboundService.checkIsEnabledByEmail(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)
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2026-03-17 21:09:49 +00:00
|
|
|
// getCommonClientButtons returns the shared inline keyboard rows for client configuration
|
|
|
|
|
func (t *Tgbot) getCommonClientButtons() [][]telego.InlineKeyboardButton {
|
|
|
|
|
return [][]telego.InlineKeyboardButton{
|
|
|
|
|
tu.InlineKeyboardRow(
|
|
|
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"),
|
|
|
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"),
|
|
|
|
|
),
|
|
|
|
|
tu.InlineKeyboardRow(
|
|
|
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"),
|
|
|
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData("add_client_ch_default_ip_limit"),
|
|
|
|
|
),
|
|
|
|
|
tu.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"),
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 11:12:54 +00:00
|
|
|
// inboundCanEnableTlsFlow mirrors Inbound.canEnableTlsFlow() from the frontend
|
|
|
|
|
// model: xtls-rprx-vision is only valid on VLESS-over-TCP with TLS or Reality.
|
|
|
|
|
func inboundCanEnableTlsFlow(ib *model.Inbound) bool {
|
|
|
|
|
if ib == nil || ib.Protocol != model.VLESS {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
var stream struct {
|
|
|
|
|
Network string `json:"network"`
|
|
|
|
|
Security string `json:"security"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal([]byte(ib.StreamSettings), &stream); err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if stream.Network != "tcp" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return stream.Security == "tls" || stream.Security == "reality"
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-20 07:35:50 +00:00
|
|
|
// addClient handles the process of adding a new client to an inbound.
|
2025-03-26 18:16:35 +00:00
|
|
|
func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
|
|
|
|
|
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.SendMsgToTgbot(chatId, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protocol := inbound.Protocol
|
|
|
|
|
|
2026-03-17 21:09:49 +00:00
|
|
|
var protocolRows [][]telego.InlineKeyboardButton
|
2025-04-06 22:45:52 +00:00
|
|
|
switch protocol {
|
2026-05-15 11:12:54 +00:00
|
|
|
case model.VMESS:
|
2026-03-17 21:09:49 +00:00
|
|
|
protocolRows = [][]telego.InlineKeyboardButton{
|
2025-03-26 18:16:35 +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_id")).WithCallbackData("add_client_ch_default_id"),
|
2025-04-06 22:45:52 +00:00
|
|
|
),
|
2025-03-26 18:16:35 +00:00
|
|
|
}
|
2026-05-15 11:12:54 +00:00
|
|
|
case model.VLESS:
|
|
|
|
|
protocolRows = [][]telego.InlineKeyboardButton{
|
|
|
|
|
tu.InlineKeyboardRow(
|
|
|
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
|
|
|
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_id")).WithCallbackData("add_client_ch_default_id"),
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
if inboundCanEnableTlsFlow(inbound) {
|
|
|
|
|
flowLabel := t.I18nBot("tgbot.buttons.change_flow")
|
|
|
|
|
if client_Flow != "" {
|
|
|
|
|
flowLabel = flowLabel + ": " + client_Flow
|
|
|
|
|
}
|
|
|
|
|
protocolRows = append(protocolRows, tu.InlineKeyboardRow(
|
|
|
|
|
tu.InlineKeyboardButton(flowLabel).WithCallbackData("add_client_ch_default_flow"),
|
|
|
|
|
))
|
|
|
|
|
} else if client_Flow != "" {
|
|
|
|
|
client_Flow = ""
|
|
|
|
|
}
|
2025-03-26 18:16:35 +00:00
|
|
|
case model.Trojan:
|
2026-03-17 21:09:49 +00:00
|
|
|
protocolRows = [][]telego.InlineKeyboardButton{
|
2025-03-26 18:16:35 +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_password")).WithCallbackData("add_client_ch_default_pass_tr"),
|
2025-04-06 22:45:52 +00:00
|
|
|
),
|
|
|
|
|
}
|
2025-03-26 18:16:35 +00:00
|
|
|
case model.Shadowsocks:
|
2026-03-17 21:09:49 +00:00
|
|
|
protocolRows = [][]telego.InlineKeyboardButton{
|
2025-03-26 18:16:35 +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_password")).WithCallbackData("add_client_ch_default_pass_sh"),
|
2025-04-06 22:45:52 +00:00
|
|
|
),
|
2025-03-26 18:16:35 +00:00
|
|
|
}
|
2025-04-06 22:45:52 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-17 21:09:49 +00:00
|
|
|
commonRows := t.getCommonClientButtons()
|
|
|
|
|
inlineKeyboard := tu.InlineKeyboard(append(protocolRows, commonRows...)...)
|
|
|
|
|
|
|
|
|
|
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(), ¶ms); 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(), ¶ms); 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(), ¶ms); 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(), ¶ms); 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)
|
|
|
|
|
}
|