tgbot_send.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. package tgbot
  2. import (
  3. "context"
  4. "fmt"
  5. "strings"
  6. "time"
  7. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  8. "github.com/mymmrac/telego"
  9. tu "github.com/mymmrac/telego/telegoutil"
  10. )
  11. // sendResponse sends the response message based on the onlyMessage flag.
  12. func (t *Tgbot) sendResponse(chatId int64, msg string, onlyMessage, isAdmin bool) {
  13. if onlyMessage {
  14. t.SendMsgToTgbot(chatId, msg)
  15. } else {
  16. t.SendAnswer(chatId, msg, isAdmin)
  17. }
  18. }
  19. // SendAnswer sends a response message with an inline keyboard to the specified chat.
  20. func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
  21. numericKeyboard := tu.InlineKeyboard(
  22. tu.InlineKeyboardRow(
  23. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.SortedTrafficUsageReport")).WithCallbackData(t.encodeQuery("get_sorted_traffic_usage_report")),
  24. ),
  25. tu.InlineKeyboardRow(
  26. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.serverUsage")).WithCallbackData(t.encodeQuery("get_usage")),
  27. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ResetAllTraffics")).WithCallbackData(t.encodeQuery("reset_all_traffics")),
  28. ),
  29. tu.InlineKeyboardRow(
  30. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.dbBackup")).WithCallbackData(t.encodeQuery("get_backup")),
  31. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.getBanLogs")).WithCallbackData(t.encodeQuery("get_banlogs")),
  32. ),
  33. tu.InlineKeyboardRow(
  34. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.getInbounds")).WithCallbackData(t.encodeQuery("inbounds")),
  35. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.depleteSoon")).WithCallbackData(t.encodeQuery("deplete_soon")),
  36. ),
  37. tu.InlineKeyboardRow(
  38. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.commands")).WithCallbackData(t.encodeQuery("commands")),
  39. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.onlines")).WithCallbackData(t.encodeQuery("onlines")),
  40. ),
  41. tu.InlineKeyboardRow(
  42. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.allClients")).WithCallbackData(t.encodeQuery("get_inbounds")),
  43. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.addClient")).WithCallbackData(t.encodeQuery("add_client")),
  44. ),
  45. tu.InlineKeyboardRow(
  46. tu.InlineKeyboardButton(t.I18nBot("pages.settings.subSettings")).WithCallbackData(t.encodeQuery("admin_client_sub_links")),
  47. tu.InlineKeyboardButton(t.I18nBot("subscription.individualLinks")).WithCallbackData(t.encodeQuery("admin_client_individual_links")),
  48. tu.InlineKeyboardButton(t.I18nBot("qrCode")).WithCallbackData(t.encodeQuery("admin_client_qr_links")),
  49. ),
  50. // TODOOOOOOOOOOOOOO: Add restart button here.
  51. )
  52. numericKeyboardClient := tu.InlineKeyboard(
  53. tu.InlineKeyboardRow(
  54. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.clientUsage")).WithCallbackData(t.encodeQuery("client_traffic")),
  55. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.commands")).WithCallbackData(t.encodeQuery("client_commands")),
  56. ),
  57. tu.InlineKeyboardRow(
  58. tu.InlineKeyboardButton(t.I18nBot("pages.settings.subSettings")).WithCallbackData(t.encodeQuery("client_sub_links")),
  59. tu.InlineKeyboardButton(t.I18nBot("subscription.individualLinks")).WithCallbackData(t.encodeQuery("client_individual_links")),
  60. ),
  61. tu.InlineKeyboardRow(
  62. tu.InlineKeyboardButton(t.I18nBot("qrCode")).WithCallbackData(t.encodeQuery("client_qr_links")),
  63. ),
  64. )
  65. var ReplyMarkup telego.ReplyMarkup
  66. if isAdmin {
  67. ReplyMarkup = numericKeyboard
  68. } else {
  69. ReplyMarkup = numericKeyboardClient
  70. }
  71. t.SendMsgToTgbot(chatId, msg, ReplyMarkup)
  72. }
  73. const telegramPageLimit = 2000
  74. func pageMessage(message string, limit int) []string {
  75. if len(message) <= limit {
  76. return []string{message}
  77. }
  78. pages := make([]string, 0)
  79. for _, block := range strings.Split(message, "\r\n\r\n") {
  80. for _, page := range splitMessageLines(block, limit) {
  81. last := len(pages) - 1
  82. if last >= 0 && len(pages[last])+len("\r\n\r\n")+len(page) <= limit {
  83. pages[last] += "\r\n\r\n" + page
  84. continue
  85. }
  86. pages = append(pages, page)
  87. }
  88. }
  89. if len(pages) > 0 && strings.TrimSpace(pages[len(pages)-1]) == "" {
  90. pages = pages[:len(pages)-1]
  91. }
  92. return pages
  93. }
  94. func splitMessageLines(block string, limit int) []string {
  95. if len(block) <= limit {
  96. return []string{block}
  97. }
  98. lines := strings.Split(block, "\r\n")
  99. pages := []string{lines[0]}
  100. for _, line := range lines[1:] {
  101. last := len(pages) - 1
  102. if len(pages[last])+len("\r\n")+len(line) > limit {
  103. pages = append(pages, line)
  104. continue
  105. }
  106. pages[last] += "\r\n" + line
  107. }
  108. return pages
  109. }
  110. // SendMsgToTgbot sends a message to the Telegram bot with optional reply markup.
  111. func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.ReplyMarkup) {
  112. if !isRunning {
  113. return
  114. }
  115. if msg == "" {
  116. logger.Info("[tgbot] message is empty!")
  117. return
  118. }
  119. allMessages := pageMessage(msg, telegramPageLimit)
  120. for n, message := range allMessages {
  121. params := telego.SendMessageParams{
  122. ChatID: tu.ID(chatId),
  123. Text: message,
  124. ParseMode: "HTML",
  125. }
  126. // only add replyMarkup to last message
  127. if len(replyMarkup) > 0 && n == (len(allMessages)-1) {
  128. params.ReplyMarkup = replyMarkup[0]
  129. }
  130. // Retry logic with exponential backoff for connection errors
  131. maxRetries := 3
  132. for attempt := range maxRetries {
  133. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  134. _, err := bot.SendMessage(ctx, &params)
  135. cancel()
  136. if err == nil {
  137. break // Success
  138. }
  139. // Check if error is a connection error
  140. errStr := err.Error()
  141. isConnectionError := strings.Contains(errStr, "connection") ||
  142. strings.Contains(errStr, "timeout") ||
  143. strings.Contains(errStr, "closed")
  144. if isConnectionError && attempt < maxRetries-1 {
  145. // Exponential backoff: 1s, 2s, 4s
  146. backoff := time.Duration(1<<uint(attempt)) * time.Second
  147. logger.Warningf("Connection error sending telegram message (attempt %d/%d), retrying in %v: %v",
  148. attempt+1, maxRetries, backoff, err)
  149. time.Sleep(backoff)
  150. } else {
  151. logger.Warning("Error sending telegram message:", err)
  152. break
  153. }
  154. }
  155. // Reduced delay to improve performance (only needed for rate limiting)
  156. if n < len(allMessages)-1 { // Only delay between messages, not after the last one
  157. time.Sleep(100 * time.Millisecond)
  158. }
  159. }
  160. }
  161. // SendMsgToTgbotAdmins sends a message to all admin Telegram chats.
  162. func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMarkup) {
  163. if len(replyMarkup) > 0 {
  164. for _, adminId := range adminIds {
  165. t.SendMsgToTgbot(adminId, msg, replyMarkup[0])
  166. }
  167. } else {
  168. for _, adminId := range adminIds {
  169. t.SendMsgToTgbot(adminId, msg)
  170. }
  171. }
  172. }
  173. // sendCallbackAnswerTgBot answers a callback query with a message.
  174. func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) {
  175. params := telego.AnswerCallbackQueryParams{
  176. CallbackQueryID: id,
  177. Text: message,
  178. }
  179. if err := bot.AnswerCallbackQuery(context.Background(), &params); err != nil {
  180. logger.Warning(err)
  181. }
  182. }
  183. // editMessageCallbackTgBot edits the reply markup of a message.
  184. func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyboard *telego.InlineKeyboardMarkup) {
  185. params := telego.EditMessageReplyMarkupParams{
  186. ChatID: tu.ID(chatId),
  187. MessageID: messageID,
  188. ReplyMarkup: inlineKeyboard,
  189. }
  190. if _, err := bot.EditMessageReplyMarkup(context.Background(), &params); err != nil {
  191. if isTelegramNotModifiedError(err) {
  192. logger.Debug("Telegram reply markup unchanged, skipping edit")
  193. return
  194. }
  195. logger.Warning(err)
  196. }
  197. }
  198. // editMessageTgBot edits the text and reply markup of a message.
  199. func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlineKeyboard ...*telego.InlineKeyboardMarkup) {
  200. params := telego.EditMessageTextParams{
  201. ChatID: tu.ID(chatId),
  202. MessageID: messageID,
  203. Text: text,
  204. ParseMode: "HTML",
  205. }
  206. if len(inlineKeyboard) > 0 {
  207. params.ReplyMarkup = inlineKeyboard[0]
  208. }
  209. if _, err := bot.EditMessageText(context.Background(), &params); err != nil {
  210. if isTelegramNotModifiedError(err) {
  211. logger.Debug("Telegram message text unchanged, skipping edit")
  212. return
  213. }
  214. logger.Warning(err)
  215. }
  216. }
  217. // Telegram answers a no-op edit with a 400 whose description carries this text;
  218. // a refresh tap that changed nothing is not an operator-visible failure.
  219. func isTelegramNotModifiedError(err error) bool {
  220. if err == nil {
  221. return false
  222. }
  223. errStr := err.Error()
  224. return strings.Contains(errStr, "not modified") ||
  225. strings.Contains(errStr, "No fields to modify")
  226. }
  227. // SendMsgToTgbotDeleteAfter sends a message and deletes it after a specified delay.
  228. func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSeconds int, replyMarkup ...telego.ReplyMarkup) {
  229. // Determine if replyMarkup was passed; otherwise, set it to nil
  230. var replyMarkupParam telego.ReplyMarkup
  231. if len(replyMarkup) > 0 {
  232. replyMarkupParam = replyMarkup[0] // Use the first element
  233. }
  234. // Send the message
  235. sentMsg, err := bot.SendMessage(context.Background(), &telego.SendMessageParams{
  236. ChatID: tu.ID(chatId),
  237. Text: msg,
  238. ReplyMarkup: replyMarkupParam, // Use the correct replyMarkup value
  239. })
  240. if err != nil {
  241. logger.Warning("Failed to send message:", err)
  242. return
  243. }
  244. // Delete the sent message after the specified number of seconds.
  245. go t.deleteMessageAfterDelay(chatId, sentMsg.MessageID, delayInSeconds)
  246. }
  247. // deleteMessageAfterDelay waits delayInSeconds and then removes the message. It
  248. // deliberately does not touch the conversation state: every caller that ends a
  249. // wizard step already clears the state synchronously, and clearing it here — up
  250. // to several seconds later — would wipe a state the user set for the next step
  251. // in the meantime, silently dropping their following input.
  252. func (t *Tgbot) deleteMessageAfterDelay(chatId int64, messageID, delayInSeconds int) {
  253. time.Sleep(time.Duration(delayInSeconds) * time.Second)
  254. t.deleteMessageTgBot(chatId, messageID)
  255. }
  256. // deleteMessageTgBot deletes a message from the chat.
  257. func (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) {
  258. if bot == nil {
  259. return
  260. }
  261. params := telego.DeleteMessageParams{
  262. ChatID: tu.ID(chatId),
  263. MessageID: messageID,
  264. }
  265. if err := bot.DeleteMessage(context.Background(), &params); err != nil {
  266. logger.Warning("Failed to delete message:", err)
  267. } else {
  268. logger.Info("Message deleted successfully")
  269. }
  270. }
  271. // TestConnection verifies the bot token is valid and the API is reachable.
  272. func (t *Tgbot) TestConnection() error {
  273. tgBotMutex.Lock()
  274. b := bot
  275. tgBotMutex.Unlock()
  276. if b == nil {
  277. return fmt.Errorf("bot not initialized")
  278. }
  279. me, err := b.GetMe(context.Background())
  280. if err != nil {
  281. return fmt.Errorf("API unreachable: %w", err)
  282. }
  283. _ = me
  284. return nil
  285. }