精简: 去掉 Telegram 机器人+通知: tgbot 包/stats-notify 与 hash 清理 job/登录通知/backuptotgbot/testTgBot/全部 tg* 设置/locale Bot 本地化/SecretClears 机制; 连带清理 Swagger/外部流量/邮件/2FA 的残留测试
This commit is contained in:
@@ -1,296 +0,0 @@
|
||||
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)
|
||||
}
|
||||
|
||||
const telegramPageLimit = 2000
|
||||
|
||||
func pageMessage(message string, limit int) []string {
|
||||
if len(message) <= limit {
|
||||
return []string{message}
|
||||
}
|
||||
|
||||
pages := make([]string, 0)
|
||||
for _, block := range strings.Split(message, "\r\n\r\n") {
|
||||
for _, page := range splitMessageLines(block, limit) {
|
||||
last := len(pages) - 1
|
||||
if last >= 0 && len(pages[last])+len("\r\n\r\n")+len(page) <= limit {
|
||||
pages[last] += "\r\n\r\n" + page
|
||||
continue
|
||||
}
|
||||
pages = append(pages, page)
|
||||
}
|
||||
}
|
||||
if len(pages) > 0 && strings.TrimSpace(pages[len(pages)-1]) == "" {
|
||||
pages = pages[:len(pages)-1]
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
func splitMessageLines(block string, limit int) []string {
|
||||
if len(block) <= limit {
|
||||
return []string{block}
|
||||
}
|
||||
|
||||
lines := strings.Split(block, "\r\n")
|
||||
pages := []string{lines[0]}
|
||||
for _, line := range lines[1:] {
|
||||
last := len(pages) - 1
|
||||
if len(pages[last])+len("\r\n")+len(line) > limit {
|
||||
pages = append(pages, line)
|
||||
continue
|
||||
}
|
||||
pages[last] += "\r\n" + line
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
allMessages := pageMessage(msg, telegramPageLimit)
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user