mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-16 09:36:07 +00:00
588a524fa5
SendMsgToTgbotDeleteAfter spawns a goroutine that, after the display delay, deleted the transient message and then unconditionally cleared the chat's conversation state. Every caller that ends a wizard step already clears the state synchronously, so that call was redundant — and harmful: if within the delay the user advanced to the next step (a callback sets a fresh awaiting_* state), the late goroutine wiped it, and the user's next message fell through unrecognized, silently dropping their input. Move the delayed deletion into deleteMessageAfterDelay, which only removes the message and no longer touches the conversation state. Guard deleteMessageTgBot against a nil bot so the deletion path is unit-testable.
276 lines
9.4 KiB
Go
276 lines
9.4 KiB
Go
package tgbot
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
|
|
"github.com/mymmrac/telego"
|
|
tu "github.com/mymmrac/telego/telegoutil"
|
|
)
|
|
|
|
// sendResponse sends the response message based on the onlyMessage flag.
|
|
func (t *Tgbot) sendResponse(chatId int64, msg string, onlyMessage, isAdmin bool) {
|
|
if onlyMessage {
|
|
t.SendMsgToTgbot(chatId, msg)
|
|
} else {
|
|
t.SendAnswer(chatId, msg, isAdmin)
|
|
}
|
|
}
|
|
|
|
// SendAnswer sends a response message with an inline keyboard to the specified chat.
|
|
func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
|
|
numericKeyboard := tu.InlineKeyboard(
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.SortedTrafficUsageReport")).WithCallbackData(t.encodeQuery("get_sorted_traffic_usage_report")),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.serverUsage")).WithCallbackData(t.encodeQuery("get_usage")),
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ResetAllTraffics")).WithCallbackData(t.encodeQuery("reset_all_traffics")),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.dbBackup")).WithCallbackData(t.encodeQuery("get_backup")),
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.getBanLogs")).WithCallbackData(t.encodeQuery("get_banlogs")),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.getInbounds")).WithCallbackData(t.encodeQuery("inbounds")),
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.depleteSoon")).WithCallbackData(t.encodeQuery("deplete_soon")),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.commands")).WithCallbackData(t.encodeQuery("commands")),
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.onlines")).WithCallbackData(t.encodeQuery("onlines")),
|
|
),
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.allClients")).WithCallbackData(t.encodeQuery("get_inbounds")),
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.addClient")).WithCallbackData(t.encodeQuery("add_client")),
|
|
),
|
|
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")),
|
|
),
|
|
// TODOOOOOOOOOOOOOO: Add restart button here.
|
|
)
|
|
numericKeyboardClient := tu.InlineKeyboard(
|
|
tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.clientUsage")).WithCallbackData(t.encodeQuery("client_traffic")),
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.commands")).WithCallbackData(t.encodeQuery("client_commands")),
|
|
),
|
|
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")),
|
|
),
|
|
)
|
|
|
|
var ReplyMarkup telego.ReplyMarkup
|
|
if isAdmin {
|
|
ReplyMarkup = numericKeyboard
|
|
} else {
|
|
ReplyMarkup = numericKeyboardClient
|
|
}
|
|
t.SendMsgToTgbot(chatId, msg, ReplyMarkup)
|
|
}
|
|
|
|
// SendMsgToTgbot sends a message to the Telegram bot with optional reply markup.
|
|
func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.ReplyMarkup) {
|
|
if !isRunning {
|
|
return
|
|
}
|
|
|
|
if msg == "" {
|
|
logger.Info("[tgbot] message is empty!")
|
|
return
|
|
}
|
|
|
|
var allMessages []string
|
|
limit := 2000
|
|
|
|
// paging message if it is big
|
|
if len(msg) > limit {
|
|
messages := strings.Split(msg, "\r\n\r\n")
|
|
lastIndex := -1
|
|
|
|
for _, message := range messages {
|
|
if (len(allMessages) == 0) || (len(allMessages[lastIndex])+len(message) > limit) {
|
|
allMessages = append(allMessages, message)
|
|
lastIndex++
|
|
} else {
|
|
allMessages[lastIndex] += "\r\n\r\n" + message
|
|
}
|
|
}
|
|
if strings.TrimSpace(allMessages[len(allMessages)-1]) == "" {
|
|
allMessages = allMessages[:len(allMessages)-1]
|
|
}
|
|
} else {
|
|
allMessages = append(allMessages, msg)
|
|
}
|
|
for n, message := range allMessages {
|
|
params := telego.SendMessageParams{
|
|
ChatID: tu.ID(chatId),
|
|
Text: message,
|
|
ParseMode: "HTML",
|
|
}
|
|
// only add replyMarkup to last message
|
|
if len(replyMarkup) > 0 && n == (len(allMessages)-1) {
|
|
params.ReplyMarkup = replyMarkup[0]
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SendMsgToTgbotAdmins sends a message to all admin Telegram chats.
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// sendCallbackAnswerTgBot answers a callback query with a message.
|
|
func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) {
|
|
params := telego.AnswerCallbackQueryParams{
|
|
CallbackQueryID: id,
|
|
Text: message,
|
|
}
|
|
if err := bot.AnswerCallbackQuery(context.Background(), ¶ms); err != nil {
|
|
logger.Warning(err)
|
|
}
|
|
}
|
|
|
|
// editMessageCallbackTgBot edits the reply markup of a message.
|
|
func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyboard *telego.InlineKeyboardMarkup) {
|
|
params := telego.EditMessageReplyMarkupParams{
|
|
ChatID: tu.ID(chatId),
|
|
MessageID: messageID,
|
|
ReplyMarkup: inlineKeyboard,
|
|
}
|
|
if _, err := bot.EditMessageReplyMarkup(context.Background(), ¶ms); err != nil {
|
|
logger.Warning(err)
|
|
}
|
|
}
|
|
|
|
// editMessageTgBot edits the text and reply markup of a message.
|
|
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",
|
|
}
|
|
if len(inlineKeyboard) > 0 {
|
|
params.ReplyMarkup = inlineKeyboard[0]
|
|
}
|
|
if _, err := bot.EditMessageText(context.Background(), ¶ms); err != nil {
|
|
logger.Warning(err)
|
|
}
|
|
}
|
|
|
|
// SendMsgToTgbotDeleteAfter sends a message and deletes it after a specified delay.
|
|
func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSeconds int, replyMarkup ...telego.ReplyMarkup) {
|
|
// 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
|
|
sentMsg, err := bot.SendMessage(context.Background(), &telego.SendMessageParams{
|
|
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 t.deleteMessageAfterDelay(chatId, sentMsg.MessageID, delayInSeconds)
|
|
}
|
|
|
|
// deleteMessageAfterDelay waits delayInSeconds and then removes the message. It
|
|
// deliberately does not touch the conversation state: every caller that ends a
|
|
// wizard step already clears the state synchronously, and clearing it here — up
|
|
// to several seconds later — would wipe a state the user set for the next step
|
|
// in the meantime, silently dropping their following input.
|
|
func (t *Tgbot) deleteMessageAfterDelay(chatId int64, messageID, delayInSeconds int) {
|
|
time.Sleep(time.Duration(delayInSeconds) * time.Second)
|
|
t.deleteMessageTgBot(chatId, messageID)
|
|
}
|
|
|
|
// deleteMessageTgBot deletes a message from the chat.
|
|
func (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) {
|
|
if bot == nil {
|
|
return
|
|
}
|
|
params := telego.DeleteMessageParams{
|
|
ChatID: tu.ID(chatId),
|
|
MessageID: messageID,
|
|
}
|
|
if err := bot.DeleteMessage(context.Background(), ¶ms); err != nil {
|
|
logger.Warning("Failed to delete message:", err)
|
|
} else {
|
|
logger.Info("Message deleted successfully")
|
|
}
|
|
}
|
|
|
|
// TestConnection verifies the bot token is valid and the API is reachable.
|
|
func (t *Tgbot) TestConnection() error {
|
|
tgBotMutex.Lock()
|
|
b := bot
|
|
tgBotMutex.Unlock()
|
|
if b == nil {
|
|
return fmt.Errorf("bot not initialized")
|
|
}
|
|
me, err := b.GetMe(context.Background())
|
|
if err != nil {
|
|
return fmt.Errorf("API unreachable: %w", err)
|
|
}
|
|
_ = me
|
|
return nil
|
|
}
|