mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-04 12:15:02 +03:00
e093854f5b
Typed commands edited the remembered menu message, which a user who cleared the chat can no longer see, and then deleted the command, so /start left the chat empty again. Typed commands (/start, /status, /usage, /clearall) now send a new screen and then remove the old menu, or retire its buttons when it is past Telegram's delete window; button taps still edit in place. Closes #6997.
1309 lines
53 KiB
Go
1309 lines
53 KiB
Go
package tgbot
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
|
|
"github.com/mymmrac/telego"
|
|
th "github.com/mymmrac/telego/telegohandler"
|
|
tu "github.com/mymmrac/telego/telegoutil"
|
|
)
|
|
|
|
// recoverBotPanic must be deferred by every bot handler entry point: telego's
|
|
// dispatch has no recovery of its own, so one bad update would kill the panel.
|
|
func recoverBotPanic() {
|
|
if r := recover(); r != nil {
|
|
logger.Error("Recovered panic in Telegram bot handler:", r)
|
|
}
|
|
}
|
|
|
|
// runBotHandler runs a bot handler on a worker slot and recovers panics: a bad
|
|
// callback must not take down the whole panel, the way a cron panic would not.
|
|
func runBotHandler(fn func()) {
|
|
messageWorkerPool <- struct{}{}
|
|
defer func() { <-messageWorkerPool }()
|
|
defer recoverBotPanic()
|
|
fn()
|
|
}
|
|
|
|
// chooseInboundClient fetches the inbound once and reuses the row: the inline
|
|
// keyboard outlives it, so a stale tap must give an error, not a panic.
|
|
func (t *Tgbot) chooseInboundClient(callbackQuery *telego.CallbackQuery, chatId int64, inboundID int, action string) {
|
|
inbound, err := t.inboundService.GetInbound(inboundID)
|
|
if err != nil {
|
|
logger.Warning("chooseInboundClient GetInbound failed:", err)
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.getInboundsFailed"))
|
|
return
|
|
}
|
|
clients, err := t.inboundService.GetClients(inbound)
|
|
if err != nil || len(clients) == 0 {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.getClientsFailed"))
|
|
return
|
|
}
|
|
rows := [][]telego.InlineKeyboardButton{}
|
|
for _, client := range clients {
|
|
rows = append(rows, tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(client.Email).WithCallbackData(t.encodeQuery(action+" "+client.Email))))
|
|
}
|
|
rows = append(rows, t.backRow())
|
|
t.answerSilent(callbackQuery.ID)
|
|
t.renderScreen(chatId, t.newScreen("clients",
|
|
t.inboundSummaryText(inbound)+"\n"+t.I18nBot("tgbot.answers.chooseClient", "Inbound=="+inbound.Remark), rows...))
|
|
}
|
|
|
|
// OnReceive starts the message receiving loop for the Telegram liveBot().
|
|
func (t *Tgbot) OnReceive() {
|
|
params := telego.GetUpdatesParams{
|
|
Timeout: 20, // Reduced timeout to detect connection issues faster
|
|
}
|
|
|
|
// Strict singleton: never start a second long-polling loop.
|
|
tgBotMutex.Lock()
|
|
if botCancel != nil || isRunning {
|
|
tgBotMutex.Unlock()
|
|
logger.Warning("TgBot OnReceive called while already running; ignoring.")
|
|
return
|
|
}
|
|
|
|
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)
|
|
tgBotMutex.Unlock()
|
|
|
|
// Get updates channel using the context with shorter timeout for better error recovery
|
|
// One bot instance for the loop and the handler it feeds: reading the global twice could diverge them.
|
|
pollBot := liveBot()
|
|
updates, _ := pollBot.UpdatesViaLongPolling(ctx, ¶ms)
|
|
go func() {
|
|
defer botWG.Done()
|
|
h, _ := th.NewBotHandler(pollBot, updates)
|
|
tgBotMutex.Lock()
|
|
botHandler = h
|
|
tgBotMutex.Unlock()
|
|
|
|
// The menu marks these as returning to a root screen, so a screen that
|
|
// carries one already offers a way out.
|
|
registerTerminalCallbacks(cbHome, cbCatServer, cbCatClients, cbCatTraffic, cbCatMaintenance)
|
|
|
|
refuseGroups(h, t)
|
|
|
|
// A picked inline item is the user's own message, caught before the wizard
|
|
// and broadcast handlers; the predicate must not match every ":" message.
|
|
h.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
|
defer recoverBotPanic()
|
|
t.handleListMarker(&message)
|
|
return nil
|
|
}, messageIsListMarker, th.AnyMessageWithFrom())
|
|
|
|
h.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
|
defer recoverBotPanic()
|
|
userStateMgr.clear(messageActor(message))
|
|
t.clearScreen(message)
|
|
return nil
|
|
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
|
|
|
|
h.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
|
defer recoverBotPanic()
|
|
if !t.isCommandForCurrentBot(&message) {
|
|
return nil
|
|
}
|
|
|
|
// Use goroutine with worker pool for concurrent command processing
|
|
go runBotHandler(func() {
|
|
userStateMgr.clear(messageActor(message))
|
|
if isAdmin, ok := t.gateCommand(&message); ok {
|
|
t.answerCommand(&message, message.Chat.ID, isAdmin)
|
|
}
|
|
// The command is litter next to the screen it opens: the bot's answer
|
|
// is worth keeping. Commands for another bot never reach here.
|
|
t.deleteIncoming(&message)
|
|
})
|
|
return nil
|
|
}, th.AnyCommand())
|
|
|
|
// Inline mode is the only list browser: no middleware is registered on it,
|
|
// since a query fires on every keystroke and would be swallowed.
|
|
h.HandleInlineQuery(func(ctx *th.Context, query telego.InlineQuery) error {
|
|
defer recoverBotPanic()
|
|
go runBotHandler(func() {
|
|
t.handleInlineQuery(&query)
|
|
})
|
|
return nil
|
|
})
|
|
|
|
h.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
|
|
// Use goroutine with worker pool for concurrent callback processing
|
|
go runBotHandler(func() {
|
|
userStateMgr.clear(callbackActor(&query))
|
|
if isAdmin, ok := t.gateCallback(&query); ok {
|
|
t.answerCallback(&query, isAdmin)
|
|
}
|
|
})
|
|
return nil
|
|
}, th.AnyCallbackQueryWithMessage())
|
|
|
|
h.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
|
defer recoverBotPanic()
|
|
userStateMgr.maybePrune(time.Hour)
|
|
actor := messageActor(message)
|
|
if userState, exists := userStateMgr.get(actor); exists {
|
|
if userState == broadcastAwaitingText {
|
|
t.handleBroadcastInput(&message, actor)
|
|
return nil
|
|
}
|
|
// Only a wizard step touches the draft, so only it takes the lock.
|
|
draft := addClientDrafts.forActor(actor)
|
|
draft.Lock()
|
|
defer draft.Unlock()
|
|
switch userState {
|
|
case "awaiting_email":
|
|
if draft.email == strings.TrimSpace(message.Text) {
|
|
t.deleteIncoming(&message)
|
|
userStateMgr.clear(actor)
|
|
return nil
|
|
}
|
|
|
|
draft.email = strings.TrimSpace(message.Text)
|
|
if t.isSingleWord(draft.email) {
|
|
userStateMgr.set(actor, "awaiting_email")
|
|
t.deleteIncoming(&message)
|
|
t.wizardInvalidInput(message.Chat.ID, draft)
|
|
} else {
|
|
t.deleteIncoming(&message)
|
|
userStateMgr.clear(actor)
|
|
t.addClient(message.Chat.ID, draft, t.BuildClientDraftMessage(draft))
|
|
}
|
|
case "awaiting_comment":
|
|
if draft.comment == strings.TrimSpace(message.Text) {
|
|
t.deleteIncoming(&message)
|
|
userStateMgr.clear(actor)
|
|
return nil
|
|
}
|
|
|
|
draft.comment = strings.TrimSpace(message.Text)
|
|
t.deleteIncoming(&message)
|
|
userStateMgr.clear(actor)
|
|
t.addClient(message.Chat.ID, draft, t.BuildClientDraftMessage(draft))
|
|
case "awaiting_tg_id":
|
|
input := strings.TrimSpace(message.Text)
|
|
if input == "" || input == "-" || strings.EqualFold(input, "none") {
|
|
draft.tgID = ""
|
|
t.deleteIncoming(&message)
|
|
userStateMgr.clear(actor)
|
|
t.addClient(message.Chat.ID, draft, t.BuildClientDraftMessage(draft))
|
|
return nil
|
|
}
|
|
if _, err := strconv.ParseInt(input, 10, 64); err != nil {
|
|
t.deleteIncoming(&message)
|
|
t.wizardInvalidInput(message.Chat.ID, draft)
|
|
return nil
|
|
}
|
|
draft.tgID = input
|
|
t.deleteIncoming(&message)
|
|
userStateMgr.clear(actor)
|
|
t.addClient(message.Chat.ID, draft, t.BuildClientDraftMessage(draft))
|
|
}
|
|
} else {
|
|
if message.UsersShared != nil {
|
|
if checkAdmin(message.From.ID) {
|
|
for _, sharedUser := range message.UsersShared.Users {
|
|
userID := sharedUser.UserID
|
|
needRestart, err := t.clientService.SetClientTelegramUserID(&t.inboundService, message.UsersShared.RequestID, userID)
|
|
if needRestart {
|
|
t.xrayService.SetToNeedRestart()
|
|
}
|
|
output := t.I18nBot("tgbot.messages.userSaved")
|
|
if err != nil {
|
|
output = t.I18nBot("tgbot.messages.selectUserFailed")
|
|
}
|
|
t.sendNoticeNoKeyboard(message.Chat.ID, output)
|
|
}
|
|
} else {
|
|
t.sendNoticeNoKeyboard(message.Chat.ID, t.I18nBot("tgbot.noResult"))
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}, th.AnyMessage())
|
|
|
|
_ = h.Start()
|
|
}()
|
|
}
|
|
|
|
// answerCommand processes incoming command messages from Telegram users.
|
|
func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) {
|
|
// onlyMessage is deliberately unused: every command path either renders a
|
|
// screen or sends a notice, both of which carry their own keyboard.
|
|
msg := ""
|
|
|
|
command, _, commandArgs := tu.ParseCommand(message.Text)
|
|
|
|
// Helper function to handle unknown commands.
|
|
handleUnknownCommand := func() {
|
|
msg += t.I18nBot("tgbot.commands.unknown")
|
|
}
|
|
|
|
// Handle the command.
|
|
switch command {
|
|
case "help":
|
|
msg += t.I18nBot("tgbot.commands.help")
|
|
msg += t.I18nBot("tgbot.commands.pleaseChoose")
|
|
case "start":
|
|
if len(commandArgs) > 0 {
|
|
// A placeholder user can be neither throttled nor bound, so its claim is
|
|
// refused before it counts: the link works from the account's own chat.
|
|
if sentForChat(message) {
|
|
t.sendNotice(chatId, t.I18nBot("tgbot.messages.inviteInvalid"))
|
|
return
|
|
}
|
|
if !isAdmin && !t.allowInviteAttempt(message.From) {
|
|
t.sendNotice(chatId, t.I18nBot("tgbot.messages.inviteRateLimited"))
|
|
return
|
|
}
|
|
t.claimInvite(chatId, message.From.ID, commandArgs[0])
|
|
}
|
|
// A stranger learns only its ChatID, which is what an admin needs to bind it.
|
|
if !isAdmin && (sentForChat(message) || t.levelOf(message.From.ID) == levelStranger) {
|
|
t.sendNotice(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+strconv.FormatInt(message.From.ID, 10)))
|
|
return
|
|
}
|
|
t.clearBroadcastPrompt(messageActor(*message))
|
|
t.screenHome(chatId, message.From.ID, screenSendNew)
|
|
return
|
|
case "status":
|
|
// The full server screen carries the host addresses and versions, so it
|
|
// stays admin-only; a client asking for status gets liveness alone.
|
|
if isAdmin {
|
|
t.screenServer(chatId, screenSendNew)
|
|
return
|
|
}
|
|
t.sendNotice(chatId, t.I18nBot("tgbot.commands.statusOk"))
|
|
return
|
|
case "id":
|
|
t.sendNotice(chatId, t.I18nBot("tgbot.commands.getID", "ID=="+strconv.FormatInt(message.From.ID, 10)))
|
|
return
|
|
case "usage":
|
|
if len(commandArgs) > 0 {
|
|
if isAdmin {
|
|
t.searchClientScreen(chatId, commandArgs[0], screenSendNew)
|
|
} else {
|
|
t.getClientUsage(chatId, message.From.ID, screenSendNew, commandArgs[0])
|
|
}
|
|
} else {
|
|
t.getClientUsage(chatId, message.From.ID, screenSendNew)
|
|
}
|
|
return
|
|
case "inbound":
|
|
if isAdmin && len(commandArgs) > 0 {
|
|
t.searchInbound(chatId, commandArgs[0])
|
|
} else {
|
|
handleUnknownCommand()
|
|
}
|
|
case "restart":
|
|
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()
|
|
}
|
|
case "clearall":
|
|
if isAdmin {
|
|
t.renderScreen(chatId, t.resetAllConfirm(), screenSendNew)
|
|
return
|
|
}
|
|
handleUnknownCommand()
|
|
case "broadcast":
|
|
if isAdmin {
|
|
t.startBroadcast(messageActor(*message))
|
|
return
|
|
}
|
|
handleUnknownCommand()
|
|
default:
|
|
handleUnknownCommand()
|
|
}
|
|
|
|
if msg != "" {
|
|
t.sendNotice(chatId, msg)
|
|
}
|
|
}
|
|
|
|
func (t *Tgbot) isCommandForCurrentBot(message *telego.Message) bool {
|
|
return isCommandForBot(message.Text, botUsername())
|
|
}
|
|
|
|
func botUsername() string {
|
|
if liveBot() == nil {
|
|
return ""
|
|
}
|
|
return liveBot().Username()
|
|
}
|
|
|
|
func isCommandForBot(text string, username string) bool {
|
|
_, commandUsername, _ := tu.ParseCommand(text)
|
|
return commandUsername == "" || username == "" || strings.EqualFold(commandUsername, username)
|
|
}
|
|
|
|
// answerCallback processes callback queries from inline keyboards.
|
|
func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {
|
|
chatId := callbackQuery.Message.GetChat().ID
|
|
actor := callbackActor(callbackQuery)
|
|
|
|
// Only an admin's wizard callbacks touch a draft, so only they take its lock:
|
|
// a report tap must not wait on a slot, a rejected chat must not be stored.
|
|
var draft *clientDraft
|
|
if isAdmin && isAddClientStep(callbackQuery.Data) {
|
|
draft = addClientDrafts.forActor(actor)
|
|
draft.Lock()
|
|
defer draft.Unlock()
|
|
}
|
|
|
|
if isAdmin {
|
|
// get query from hash storage
|
|
decodedQuery, err := t.decodeQuery(callbackQuery.Data)
|
|
if err != nil {
|
|
// A button older than the 20-minute hash window is the common case
|
|
// here; the answer clears it, the message outlives a failed send.
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.noQuery"))
|
|
t.sendNotice(chatId, t.I18nBot("tgbot.noQuery"))
|
|
return
|
|
}
|
|
dataArray := strings.Split(decodedQuery, " ")
|
|
|
|
if len(dataArray) >= 2 && len(dataArray[1]) > 0 {
|
|
email := dataArray[1]
|
|
switch dataArray[0] {
|
|
case "broadcast_confirm":
|
|
t.confirmBroadcast(actor, dataArray[1], callbackQuery.Message.GetMessageID(), callbackQuery.ID)
|
|
case "get_clients_for_sub":
|
|
inboundIdInt, err := strconv.Atoi(dataArray[1])
|
|
if err != nil {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
return
|
|
}
|
|
t.chooseInboundClient(callbackQuery, chatId, inboundIdInt, "client_sub_links")
|
|
case "get_clients_for_individual":
|
|
inboundIdInt, err := strconv.Atoi(dataArray[1])
|
|
if err != nil {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
return
|
|
}
|
|
t.chooseInboundClient(callbackQuery, chatId, inboundIdInt, "client_individual_links")
|
|
case "get_clients_for_qr":
|
|
inboundIdInt, err := strconv.Atoi(dataArray[1])
|
|
if err != nil {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
return
|
|
}
|
|
t.chooseInboundClient(callbackQuery, chatId, inboundIdInt, "client_qr_links")
|
|
case "client_sub_links":
|
|
t.screenClientLinks(chatId, email)
|
|
return
|
|
case "client_individual_links":
|
|
t.screenIndividualLinks(chatId, email)
|
|
return
|
|
case "client_qr_links":
|
|
t.screenClientQR(chatId, email)
|
|
return
|
|
case "client_get_usage":
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.messages.email", "Email=="+email))
|
|
t.searchClientScreen(chatId, email)
|
|
case "client_refresh":
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clientRefreshSuccess", "Email=="+email))
|
|
t.searchClientScreen(chatId, email)
|
|
case "client_cancel":
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email))
|
|
t.searchClientScreen(chatId, email)
|
|
case "ips_refresh":
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.IpRefreshSuccess", "Email=="+email))
|
|
t.screenClientIps(chatId, email)
|
|
case "ips_cancel":
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email))
|
|
t.screenClientIps(chatId, email)
|
|
case "tgid_refresh":
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.TGIdRefreshSuccess", "Email=="+email))
|
|
t.screenClientTG(chatId, email, false)
|
|
case "tgid_cancel":
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email))
|
|
t.screenClientTG(chatId, email, false)
|
|
case "reset_traffic":
|
|
inlineKeyboard := tu.InlineKeyboard(
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancelReset")).WithCallbackData(t.encodeQuery("client_cancel "+email)),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmResetTraffic")).WithCallbackData(t.encodeQuery("reset_traffic_c "+email)),
|
|
),
|
|
)
|
|
t.renderPickerOnClient(chatId, email, inlineKeyboard.InlineKeyboard)
|
|
case "reset_traffic_c":
|
|
err := t.inboundService.ResetClientTrafficByEmail(email)
|
|
if err == nil {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetTrafficSuccess", "Email=="+email))
|
|
t.searchClientScreen(chatId, email)
|
|
} else {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
|
}
|
|
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(
|
|
tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 50")),
|
|
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")),
|
|
),
|
|
)
|
|
t.renderPickerOnClient(chatId, email, inlineKeyboard.InlineKeyboard)
|
|
case "limit_traffic_c":
|
|
if len(dataArray) == 3 {
|
|
limitTraffic, err := strconv.Atoi(dataArray[2])
|
|
if err == nil {
|
|
needRestart, err := t.clientService.ResetClientTrafficLimitByEmail(&t.inboundService, email, limitTraffic)
|
|
if needRestart {
|
|
t.xrayService.SetToNeedRestart()
|
|
}
|
|
if err == nil {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.setTrafficLimitSuccess", "Email=="+email))
|
|
t.searchClientScreen(chatId, email)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
|
t.searchClientScreen(chatId, email)
|
|
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 {
|
|
inputNumber = updateNumericInput(inputNumber, 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 := t.numericKeypad(numericKeypadSpec{dataBase: "limit_traffic", dataArgs: email + " ", cancelData: "client_cancel " + email, confirmLabelKey: "tgbot.buttons.confirmNumberAdd"}, inputNumber)
|
|
t.renderPickerOnClient(chatId, email, inlineKeyboard.InlineKeyboard)
|
|
return
|
|
}
|
|
}
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
|
t.searchClientScreen(chatId, email)
|
|
case "add_client_limit_traffic_c":
|
|
limitTraffic, _ := strconv.ParseInt(dataArray[1], 10, 64)
|
|
draft.totalGB = limitTraffic * 1024 * 1024 * 1024
|
|
messageId := callbackQuery.Message.GetMessageID()
|
|
message_text := t.BuildClientDraftMessage(draft)
|
|
|
|
t.addClient(callbackQuery.Message.GetChat().ID, draft, message_text, messageId)
|
|
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 {
|
|
inputNumber = updateNumericInput(inputNumber, 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 := t.numericKeypad(numericKeypadSpec{dataBase: "add_client_limit_traffic", cancelData: "add_client_default_traffic_exp", confirmLabelKey: "tgbot.buttons.confirmNumberAdd"}, inputNumber)
|
|
t.renderPickerOnClient(chatId, email, inlineKeyboard.InlineKeyboard)
|
|
return
|
|
}
|
|
}
|
|
case "reset_exp":
|
|
inlineKeyboard := tu.InlineKeyboard(
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancelReset")).WithCallbackData(t.encodeQuery("client_cancel "+email)),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.unlimited")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 0")),
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.custom")).WithCallbackData(t.encodeQuery("reset_exp_in "+email+" 0")),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
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")),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
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")),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
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")),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
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")),
|
|
),
|
|
)
|
|
t.renderPickerOnClient(chatId, email, inlineKeyboard.InlineKeyboard)
|
|
case "reset_exp_c":
|
|
if len(dataArray) == 3 {
|
|
days, err := strconv.ParseInt(dataArray[2], 10, 64)
|
|
if err == nil {
|
|
var date int64
|
|
if days > 0 {
|
|
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
|
|
if err != nil {
|
|
logger.Warning(err)
|
|
t.sendNotice(chatId, t.I18nBot("tgbot.wentWrong"))
|
|
return
|
|
}
|
|
if traffic == nil {
|
|
t.sendNotice(chatId, t.I18nBot("tgbot.noResult"))
|
|
return
|
|
}
|
|
|
|
if traffic.ExpiryTime > 0 {
|
|
if traffic.ExpiryTime-time.Now().Unix()*1000 < 0 {
|
|
date = -(days * 24 * 60 * 60000)
|
|
} else {
|
|
date = traffic.ExpiryTime + days*24*60*60000
|
|
}
|
|
} else {
|
|
date = traffic.ExpiryTime - days*24*60*60000
|
|
}
|
|
|
|
}
|
|
needRestart, err := t.clientService.ResetClientExpiryTimeByEmail(&t.inboundService, email, date)
|
|
if needRestart {
|
|
t.xrayService.SetToNeedRestart()
|
|
}
|
|
if err == nil {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.expireResetSuccess", "Email=="+email))
|
|
t.searchClientScreen(chatId, email)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
|
t.searchClientScreen(chatId, email)
|
|
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 {
|
|
inputNumber = updateNumericInput(inputNumber, 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 := t.numericKeypad(numericKeypadSpec{dataBase: "reset_exp", dataArgs: email + " ", cancelData: "client_cancel " + email, confirmLabelKey: "tgbot.buttons.confirmNumber"}, inputNumber)
|
|
t.renderPickerOnClient(chatId, email, inlineKeyboard.InlineKeyboard)
|
|
return
|
|
}
|
|
}
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
|
t.searchClientScreen(chatId, email)
|
|
case "add_client_reset_exp_c":
|
|
// The wizard's presets and its custom keypad land in this one case, so a
|
|
// second tap replaces the term it set; 0 is the Unlimited button.
|
|
days, _ := strconv.ParseInt(dataArray[1], 10, 64)
|
|
draft.expiryTime = -days * 24 * 60 * 60000
|
|
|
|
messageId := callbackQuery.Message.GetMessageID()
|
|
message_text := t.BuildClientDraftMessage(draft)
|
|
|
|
t.addClient(callbackQuery.Message.GetChat().ID, draft, message_text, messageId)
|
|
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 {
|
|
inputNumber = updateNumericInput(inputNumber, 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 := t.numericKeypad(numericKeypadSpec{dataBase: "add_client_reset_exp", cancelData: "add_client_default_traffic_exp", confirmLabelKey: "tgbot.buttons.confirmNumberAdd"}, inputNumber)
|
|
t.renderPickerOnClient(chatId, email, inlineKeyboard.InlineKeyboard)
|
|
return
|
|
}
|
|
}
|
|
case "ip_limit":
|
|
inlineKeyboard := tu.InlineKeyboard(
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancelIpLimit")).WithCallbackData(t.encodeQuery("client_cancel "+email)),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.unlimited")).WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 0")),
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.custom")).WithCallbackData(t.encodeQuery("ip_limit_in "+email+" 0")),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 1")),
|
|
tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 2")),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 3")),
|
|
tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 4")),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
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")),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
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")),
|
|
),
|
|
)
|
|
t.renderPickerOnClient(chatId, email, inlineKeyboard.InlineKeyboard)
|
|
case "ip_limit_c":
|
|
if len(dataArray) == 3 {
|
|
count, err := strconv.Atoi(dataArray[2])
|
|
if err == nil {
|
|
needRestart, err := t.clientService.ResetClientIpLimitByEmail(&t.inboundService, email, count)
|
|
if needRestart {
|
|
t.xrayService.SetToNeedRestart()
|
|
}
|
|
if err == nil {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetIpSuccess", "Email=="+email, "Count=="+strconv.Itoa(count)))
|
|
t.searchClientScreen(chatId, email)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
|
t.searchClientScreen(chatId, email)
|
|
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 {
|
|
inputNumber = updateNumericInput(inputNumber, 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 := t.numericKeypad(numericKeypadSpec{dataBase: "ip_limit", dataArgs: email + " ", cancelData: "client_cancel " + email, confirmLabelKey: "tgbot.buttons.confirmNumber"}, inputNumber)
|
|
t.renderPickerOnClient(chatId, email, inlineKeyboard.InlineKeyboard)
|
|
return
|
|
}
|
|
}
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
|
t.searchClientScreen(chatId, email)
|
|
case "add_client_ip_limit_c":
|
|
if len(dataArray) == 2 {
|
|
count, _ := strconv.Atoi(dataArray[1])
|
|
draft.limitIP = count
|
|
}
|
|
|
|
messageId := callbackQuery.Message.GetMessageID()
|
|
message_text := t.BuildClientDraftMessage(draft)
|
|
|
|
t.addClient(callbackQuery.Message.GetChat().ID, draft, message_text, messageId)
|
|
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 {
|
|
inputNumber = updateNumericInput(inputNumber, 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 := t.numericKeypad(numericKeypadSpec{dataBase: "add_client_ip_limit", cancelData: "add_client_default_ip_limit", confirmLabelKey: "tgbot.buttons.confirmNumber"}, inputNumber)
|
|
t.renderPickerOnClient(chatId, email, inlineKeyboard.InlineKeyboard)
|
|
return
|
|
}
|
|
}
|
|
case "clear_ips":
|
|
inlineKeyboard := tu.InlineKeyboard(
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("ips_cancel "+email)),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmClearIps")).WithCallbackData(t.encodeQuery("clear_ips_c "+email)),
|
|
),
|
|
)
|
|
t.renderPickerOnClient(chatId, email, inlineKeyboard.InlineKeyboard)
|
|
case "clear_ips_c":
|
|
err := t.inboundService.ClearClientIps(email)
|
|
if err == nil {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clearIpSuccess", "Email=="+email))
|
|
t.screenClientIps(chatId, email)
|
|
} else {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
|
t.screenClientIps(chatId, email)
|
|
}
|
|
case "ip_log":
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.getIpLog", "Email=="+email))
|
|
t.screenClientIps(chatId, email)
|
|
case "tgid_pick":
|
|
userStateMgr.set(actor, "awaiting_tg_pick")
|
|
t.answerSilent(callbackQuery.ID)
|
|
t.screenClientTG(chatId, email, false)
|
|
t.sendTGPicker(chatId, t.clientTrafficID(email))
|
|
case "tg_user":
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.getUserInfo", "Email=="+email))
|
|
t.screenClientTG(chatId, email, false)
|
|
case "client_invite_link":
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.inviteLink"))
|
|
t.screenInviteLink(chatId, email)
|
|
case "tgid_remove":
|
|
inlineKeyboard := tu.InlineKeyboard(
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("tgid_cancel "+email)),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmRemoveTGUser")).WithCallbackData(t.encodeQuery("tgid_remove_c "+email)),
|
|
),
|
|
)
|
|
t.renderPickerOnClient(chatId, email, inlineKeyboard.InlineKeyboard)
|
|
case "tgid_remove_c":
|
|
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
|
|
if err != nil || traffic == nil {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
|
return
|
|
}
|
|
needRestart, err := t.clientService.SetClientTelegramUserID(&t.inboundService, traffic.Id, EmptyTelegramUserID)
|
|
if needRestart {
|
|
t.xrayService.SetToNeedRestart()
|
|
}
|
|
if err == nil {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.removedTGUserSuccess", "Email=="+email))
|
|
t.screenClientTG(chatId, email, false)
|
|
} else {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
|
t.screenClientTG(chatId, email, false)
|
|
}
|
|
case "toggle_enable":
|
|
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)),
|
|
),
|
|
)
|
|
t.renderPickerOnClient(chatId, email, inlineKeyboard.InlineKeyboard)
|
|
case "toggle_enable_c":
|
|
enabled, needRestart, err := t.clientService.ToggleClientEnableByEmail(&t.inboundService, email)
|
|
if needRestart {
|
|
t.xrayService.SetToNeedRestart()
|
|
}
|
|
if err == nil {
|
|
if enabled {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.enableSuccess", "Email=="+email))
|
|
} else {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.disableSuccess", "Email=="+email))
|
|
}
|
|
t.searchClientScreen(chatId, email)
|
|
} else {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
|
t.searchClientScreen(chatId, email)
|
|
}
|
|
case "get_clients":
|
|
inboundIdInt, err := strconv.Atoi(dataArray[1])
|
|
if err != nil {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
return
|
|
}
|
|
t.answerSilent(callbackQuery.ID)
|
|
t.screenInboundClients(chatId, inboundIdInt)
|
|
case "add_client_to":
|
|
inboundIdInt, err := strconv.Atoi(dataArray[1])
|
|
if err != nil {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
return
|
|
}
|
|
t.resetDraft(draft)
|
|
draft.receiverInboundID = inboundIdInt
|
|
draft.receiverInboundIDs = []int{inboundIdInt}
|
|
t.addClient(callbackQuery.Message.GetChat().ID, draft, t.BuildClientDraftMessage(draft))
|
|
case "add_client_toggle_attach":
|
|
inboundIdStr := dataArray[1]
|
|
inboundIdInt, err := strconv.Atoi(inboundIdStr)
|
|
if err != nil {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
return
|
|
}
|
|
found := -1
|
|
for i, id := range draft.receiverInboundIDs {
|
|
if id == inboundIdInt {
|
|
found = i
|
|
break
|
|
}
|
|
}
|
|
if found >= 0 {
|
|
draft.receiverInboundIDs = append(draft.receiverInboundIDs[:found], draft.receiverInboundIDs[found+1:]...)
|
|
} else {
|
|
draft.receiverInboundIDs = append(draft.receiverInboundIDs, inboundIdInt)
|
|
}
|
|
picker, err := t.getInboundsAttachPicker(draft)
|
|
if err != nil {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
return
|
|
}
|
|
t.renderDraftPicker(callbackQuery.Message.GetChat().ID, draft, picker.InlineKeyboard)
|
|
default:
|
|
// An unknown action with arguments is still a tap, and an
|
|
// unanswered tap spins until Telegram times it out.
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
|
}
|
|
return
|
|
} else {
|
|
switch callbackQuery.Data {
|
|
case "broadcast_cancel":
|
|
t.cancelBroadcast(actor, callbackQuery.Message.GetMessageID(), callbackQuery.ID)
|
|
// The broadcast step owns its own message; nothing else runs.
|
|
return
|
|
case "get_inbounds":
|
|
t.screenInbounds(chatId, callbackQuery.From.ID)
|
|
case "admin_client_sub_links", "admin_client_individual_links", "admin_client_qr_links":
|
|
t.browseInboundsScreen(chatId, callbackQuery.From.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !isAdmin {
|
|
// encodeQuery hashes any payload past 64 chars, so a long email's button
|
|
// must be decoded before the gate can see which client it names.
|
|
if decoded, err := t.decodeQuery(callbackQuery.Data); err == nil {
|
|
callbackQuery.Data = decoded
|
|
}
|
|
if !isClientSelfCallback(callbackQuery.Data) {
|
|
return
|
|
}
|
|
}
|
|
|
|
switch callbackQuery.Data {
|
|
case "home":
|
|
t.screenHome(chatId, callbackQuery.From.ID)
|
|
case cbCatServer, cbCatClients, cbCatTraffic, cbCatMaintenance:
|
|
t.screenCategory(chatId, strings.TrimPrefix(callbackQuery.Data, "cat:"))
|
|
case "act":
|
|
// The old "Actions" screen folded into the Maintenance category.
|
|
t.screenCategory(chatId, "maintenance")
|
|
case "hide":
|
|
t.hideMessage(callbackQuery)
|
|
case "pg:next":
|
|
t.turnPage(callbackQuery, 1)
|
|
case "pg:prev":
|
|
t.turnPage(callbackQuery, -1)
|
|
case "pg:none":
|
|
t.answerSilent(callbackQuery.ID)
|
|
case "srv":
|
|
t.answerSilent(callbackQuery.ID)
|
|
t.screenServer(chatId)
|
|
case "inb", "inbounds", "get_inbounds":
|
|
t.screenInbounds(chatId, callbackQuery.From.ID)
|
|
case "cli":
|
|
t.answerSilent(callbackQuery.ID)
|
|
t.screenAllClients(chatId)
|
|
case "onl", "onlines", "onlines_refresh":
|
|
t.answerSilent(callbackQuery.ID)
|
|
t.screenOnlines(chatId)
|
|
case "dep", "deplete_soon":
|
|
t.screenDeplete(chatId)
|
|
case "rep", "get_sorted_traffic_usage_report":
|
|
t.screenReport(chatId)
|
|
case "bkp", "get_backup":
|
|
t.answerSilent(callbackQuery.ID)
|
|
t.sendBackupScreen(chatId)
|
|
case "ban", "get_banlogs":
|
|
t.answerSilent(callbackQuery.ID)
|
|
t.screenBanLogs(chatId)
|
|
case "inline_help":
|
|
t.answerSilent(callbackQuery.ID)
|
|
t.screenInlineHelp(chatId)
|
|
case "inline_recheck":
|
|
t.refreshInlineCapability()
|
|
if t.inlineSupported() {
|
|
t.screenInbounds(chatId, callbackQuery.From.ID)
|
|
} else {
|
|
t.screenInlineHelp(chatId)
|
|
}
|
|
case "cmd", "commands", "client_commands":
|
|
t.screenCommands(chatId, isAdmin)
|
|
case "addc", "add_client":
|
|
t.screenAddClientStart(chatId, callbackQuery.From.ID)
|
|
case cbBroadcast:
|
|
// The broadcast flow already existed behind /broadcast; the button is
|
|
// the discoverable way in. It owns its own messages, so no screen here.
|
|
t.answerSilent(callbackQuery.ID)
|
|
t.startBroadcast(actor)
|
|
case "xray_restart":
|
|
t.restartXrayFromScreen(callbackQuery)
|
|
case "rst_all", "reset_all_traffics":
|
|
t.renderScreen(chatId, t.resetAllConfirm())
|
|
case "client_traffic":
|
|
t.answerSilent(callbackQuery.ID)
|
|
t.getClientUsage(chatId, callbackQuery.From.ID, screenEditOrReplace)
|
|
case "client_sub_links", "client_individual_links", "client_qr_links":
|
|
// A client tapped its own link button: its identity already says which
|
|
// subscription, so it opens directly. A second one still needs a choice.
|
|
t.openOwnClientScreen(chatId, callbackQuery.From.ID, callbackQuery.Data)
|
|
case "add_client_ch_default_email":
|
|
userStateMgr.set(actor, "awaiting_email")
|
|
t.wizardPrompt(chatId, draft, t.I18nBot("tgbot.messages.email_prompt", "ClientEmail=="+draft.email))
|
|
case "add_client_ch_default_comment":
|
|
userStateMgr.set(actor, "awaiting_comment")
|
|
t.wizardPrompt(chatId, draft, t.I18nBot("tgbot.messages.comment_prompt", "ClientComment=="+draft.comment))
|
|
case "add_client_ch_default_tg_id":
|
|
userStateMgr.set(actor, "awaiting_tg_id")
|
|
current := draft.tgID
|
|
if current == "" {
|
|
current = "—"
|
|
}
|
|
t.wizardPrompt(chatId, draft, t.I18nBot("tgbot.messages.tgid_prompt", "ClientTGID=="+current))
|
|
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.renderDraftPicker(chatId, draft, inlineKeyboard.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")),
|
|
),
|
|
// No "Add" verb: these replace the term the draft carries, unlike the
|
|
// renewal keyboard, whose reset_exp_c handler really does add to it.
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton("7 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 7")),
|
|
tu.InlineKeyboardButton("10 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 10")),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton("14 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 14")),
|
|
tu.InlineKeyboardButton("20 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 20")),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton("1 "+t.I18nBot("tgbot.month")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 30")),
|
|
tu.InlineKeyboardButton("3 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 90")),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton("6 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 180")),
|
|
tu.InlineKeyboardButton("12 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 365")),
|
|
),
|
|
)
|
|
t.renderDraftPicker(chatId, draft, inlineKeyboard.InlineKeyboard)
|
|
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.renderDraftPicker(chatId, draft, inlineKeyboard.InlineKeyboard)
|
|
case "add_client_default_info":
|
|
userStateMgr.clear(actor)
|
|
t.addClient(chatId, draft, t.BuildClientDraftMessage(draft), callbackQuery.Message.GetMessageID())
|
|
case "add_client_cancel":
|
|
userStateMgr.clear(actor)
|
|
addClientDrafts.reset(actor)
|
|
t.renderScreen(chatId, t.newScreen("main", t.I18nBot("tgbot.messages.cancel"), t.homeRows()...))
|
|
case "add_client_default_traffic_exp", "add_client_default_ip_limit":
|
|
// The picker was cancelled: the draft comes back unchanged.
|
|
t.addClient(chatId, draft, t.BuildClientDraftMessage(draft), callbackQuery.Message.GetMessageID())
|
|
case "add_client_attach_more":
|
|
picker, err := t.getInboundsAttachPicker(draft)
|
|
if err != nil {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
|
return
|
|
}
|
|
t.renderDraftPicker(chatId, draft, picker.InlineKeyboard)
|
|
case "add_client_attach_done":
|
|
if draft.receiverInboundID == 0 && len(draft.receiverInboundIDs) > 0 {
|
|
draft.receiverInboundID = draft.receiverInboundIDs[0]
|
|
}
|
|
if draft.receiverInboundID == 0 {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.getInboundsFailed"))
|
|
return
|
|
}
|
|
t.addClient(chatId, draft, t.BuildClientDraftMessage(draft), callbackQuery.Message.GetMessageID())
|
|
case "add_client_submit_disable", "add_client_submit_enable":
|
|
// Both submits land here and differ only in the enable flag; the data is
|
|
// short enough never to be hashed, so its raw form is the discriminator.
|
|
draft.enable = callbackQuery.Data == "add_client_submit_enable"
|
|
if _, err := t.SubmitAddClient(draft); err != nil {
|
|
t.sendNotice(chatId, t.I18nBot("tgbot.messages.error_add_client", "error=="+err.Error()))
|
|
return
|
|
}
|
|
email := draft.email
|
|
addClientDrafts.reset(actor)
|
|
// The new client's card is the screen: the links and the QR hang off it,
|
|
// so nobody has to go looking for the client they just created.
|
|
t.searchClientScreen(chatId, email)
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
|
|
case "reset_all_traffics_cancel":
|
|
t.renderScreen(chatId, t.newScreen("main", t.I18nBot("tgbot.messages.cancel"), t.homeRows()...))
|
|
case "reset_all_traffics_c":
|
|
emails, err := t.inboundService.GetAllEmails()
|
|
if err != nil {
|
|
t.sendNotice(chatId, t.I18nBot("tgbot.answers.errorOperation"))
|
|
return
|
|
}
|
|
|
|
// One report per tap, not one message per client: a large panel would
|
|
// otherwise burst past Telegram's rate limit.
|
|
var report strings.Builder
|
|
for _, email := range emails {
|
|
if err := t.inboundService.ResetClientTrafficByEmail(email); err == nil {
|
|
report.WriteString(t.I18nBot("tgbot.messages.SuccessResetTraffic", "ClientEmail=="+email))
|
|
} else {
|
|
report.WriteString(t.I18nBot("tgbot.messages.FailedResetTraffic", "ClientEmail=="+email, "ErrorMessage=="+err.Error()))
|
|
}
|
|
report.WriteString("\r\n\r\n")
|
|
}
|
|
report.WriteString(t.I18nBot("tgbot.messages.FinishProcess"))
|
|
|
|
// Paged: past ~80 clients the report exceeds one message.
|
|
t.sendPagedNotice(chatId, report.String())
|
|
// The confirmation has served its purpose: leaving it on screen left the
|
|
// only button being the one just pressed.
|
|
t.screenHome(chatId, callbackQuery.From.ID)
|
|
default:
|
|
action, email, ok := splitClientLinkCallback(callbackQuery.Data)
|
|
if !ok {
|
|
// Nothing matched: an unknown button still has to be answered, or it
|
|
// keeps spinning until Telegram times the callback out.
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
|
return
|
|
}
|
|
// The keyboard outlives the chat it was sent to, so the email in it
|
|
// cannot authorise itself: a non-admin only reaches their own clients.
|
|
if !isAdmin && !t.clientOwnedByTgUser(callbackQuery.From.ID, email) {
|
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
|
return
|
|
}
|
|
switch action {
|
|
case "client_sub_links":
|
|
t.screenClientLinks(chatId, email)
|
|
case "client_individual_links":
|
|
t.screenIndividualLinks(chatId, email)
|
|
case "client_qr_links":
|
|
t.screenClientQR(chatId, email)
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkAdmin checks if the given Telegram ID is an admin.
|
|
func checkAdmin(tgId int64) bool {
|
|
return slices.Contains(adminSnapshot(), tgId)
|
|
}
|
|
|
|
// isClientSelfCallback reports whether a callback is per-user rather than
|
|
// admin-only; the caller still has to prove the client is its own.
|
|
func isClientSelfCallback(data string) bool {
|
|
switch data {
|
|
// "hide" is how any recipient puts a message away: it carries no target of its
|
|
// own (it is the message it sits on), so it is safe for anyone to reach.
|
|
case "home", "hide", "client_traffic", "client_commands", "client_sub_links",
|
|
"client_individual_links", "client_qr_links":
|
|
return true
|
|
// Paging only turns the screen already tracked for the caller's own chat.
|
|
case cbPageNext, cbPagePrev, cbPageNone:
|
|
return true
|
|
}
|
|
_, _, ok := splitClientLinkCallback(data)
|
|
return ok
|
|
}
|
|
|
|
// splitClientLinkCallback splits "<action> <email>" for the per-client link
|
|
// callbacks; ok is false for every other data.
|
|
func splitClientLinkCallback(data string) (action, email string, ok bool) {
|
|
for _, candidate := range []string{"client_sub_links", "client_individual_links", "client_qr_links"} {
|
|
if rest, found := strings.CutPrefix(data, candidate+" "); found && rest != "" {
|
|
return candidate, rest, true
|
|
}
|
|
}
|
|
return "", "", false
|
|
}
|
|
|
|
// refuseGroups keeps every update in a private chat: a group shares one screen and keyboard among its
|
|
// members, so admins and clients would overwrite each other.
|
|
func refuseGroups(h *th.BotHandler, t *Tgbot) {
|
|
h.Use(func(ctx *th.Context, update telego.Update) error {
|
|
if chatType := updateChatType(update); chatType != "" && chatType != telego.ChatTypePrivate {
|
|
t.refuseNonPrivateChat(update)
|
|
return nil
|
|
}
|
|
return ctx.Next(update)
|
|
})
|
|
}
|
|
|
|
// updateChatType reports a message or callback chat kind, "channel" for an inline query from a channel the
|
|
// sender is not in; an inline query's "sender" type counts as private.
|
|
func updateChatType(update telego.Update) string {
|
|
switch {
|
|
case update.Message != nil:
|
|
return update.Message.Chat.Type
|
|
case update.CallbackQuery != nil && update.CallbackQuery.Message != nil:
|
|
return update.CallbackQuery.Message.GetChat().Type
|
|
case update.InlineQuery != nil:
|
|
if update.InlineQuery.ChatType == "" || update.InlineQuery.ChatType == "sender" {
|
|
return telego.ChatTypePrivate
|
|
}
|
|
return update.InlineQuery.ChatType
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// refuseNonPrivateChat answers a group or channel that the bot refuses to serve.
|
|
func (t *Tgbot) refuseNonPrivateChat(update telego.Update) {
|
|
if update.InlineQuery != nil {
|
|
t.answerInline(update.InlineQuery.ID, nil, "")
|
|
return
|
|
}
|
|
chatID := int64(0)
|
|
if update.Message != nil {
|
|
chatID = update.Message.Chat.ID
|
|
} else if update.CallbackQuery != nil && update.CallbackQuery.Message != nil {
|
|
chatID = update.CallbackQuery.Message.GetChat().ID
|
|
t.answerSilent(update.CallbackQuery.ID)
|
|
}
|
|
if chatID != 0 {
|
|
t.sendNoticeNoKeyboard(chatID, t.I18nBot("tgbot.messages.privateOnly"))
|
|
}
|
|
}
|