tgbot_report.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. package tgbot
  2. import (
  3. "context"
  4. "fmt"
  5. "net"
  6. "os"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "github.com/mhsanaei/3x-ui/v3/internal/config"
  11. "github.com/mhsanaei/3x-ui/v3/internal/database"
  12. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  13. "github.com/mhsanaei/3x-ui/v3/internal/eventbus"
  14. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  15. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  16. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  17. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  18. "github.com/mymmrac/telego"
  19. tu "github.com/mymmrac/telego/telegoutil"
  20. )
  21. // SendReport sends a periodic report to admin chats.
  22. func (t *Tgbot) SendReport() {
  23. runTime, err := t.settingService.GetTgbotRuntime()
  24. if err == nil && len(runTime) > 0 {
  25. msg := ""
  26. msg += t.I18nBot("tgbot.messages.report", "RunTime=="+runTime)
  27. msg += t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05"))
  28. t.SendMsgToTgbotAdmins(msg)
  29. }
  30. info := t.sendServerUsage()
  31. t.SendMsgToTgbotAdmins(info)
  32. t.sendExhaustedToAdmins()
  33. t.notifyExhausted()
  34. backupEnable, err := t.settingService.GetTgBotBackup()
  35. if err == nil && backupEnable {
  36. t.SendBackupToAdmins()
  37. }
  38. }
  39. // SendBackupToAdmins sends a database backup to admin chats.
  40. func (t *Tgbot) SendBackupToAdmins() {
  41. if !t.IsRunning() {
  42. return
  43. }
  44. for i, adminId := range adminIds {
  45. t.sendBackup(int64(adminId))
  46. // Add delay between sends to avoid Telegram rate limits
  47. if i < len(adminIds)-1 {
  48. time.Sleep(1 * time.Second)
  49. }
  50. }
  51. }
  52. // sendExhaustedToAdmins sends notifications about exhausted clients to admins.
  53. func (t *Tgbot) sendExhaustedToAdmins() {
  54. if !t.IsRunning() {
  55. return
  56. }
  57. for _, adminId := range adminIds {
  58. t.getExhausted(int64(adminId))
  59. }
  60. }
  61. // getServerUsage retrieves and formats server usage information.
  62. func (t *Tgbot) getServerUsage(chatId int64, messageID ...int) string {
  63. info := t.prepareServerUsageInfo()
  64. keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow(
  65. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("usage_refresh"))))
  66. if len(messageID) > 0 {
  67. t.editMessageTgBot(chatId, messageID[0], info, keyboard)
  68. } else {
  69. t.SendMsgToTgbot(chatId, info, keyboard)
  70. }
  71. return info
  72. }
  73. // Send server usage without an inline keyboard
  74. func (t *Tgbot) sendServerUsage() string {
  75. info := t.prepareServerUsageInfo()
  76. return info
  77. }
  78. // prepareServerUsageInfo prepares the server usage information string.
  79. func (t *Tgbot) prepareServerUsageInfo() string {
  80. // Check if we have cached data first
  81. if cachedStats, found := t.getCachedServerStats(); found {
  82. return cachedStats
  83. }
  84. info, ipv4, ipv6 := "", "", ""
  85. // get latest status of server with caching
  86. if cachedStatus, found := t.getCachedStatus(); found {
  87. t.lastStatus = cachedStatus
  88. } else {
  89. t.lastStatus = t.serverService.GetStatus(t.lastStatus)
  90. t.setCachedStatus(t.lastStatus)
  91. }
  92. onlines := service.XrayProcess().GetOnlineClients()
  93. info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
  94. info += t.I18nBot("tgbot.messages.version", "Version=="+config.GetVersion())
  95. info += t.I18nBot("tgbot.messages.xrayVersion", "XrayVersion=="+fmt.Sprint(t.lastStatus.Xray.Version))
  96. // get ip address
  97. netInterfaces, err := net.Interfaces()
  98. if err != nil {
  99. logger.Error("net.Interfaces failed, err: ", err.Error())
  100. info += t.I18nBot("tgbot.messages.ip", "IP=="+t.I18nBot("tgbot.unknown"))
  101. info += "\r\n"
  102. } else {
  103. for i := range netInterfaces {
  104. if (netInterfaces[i].Flags & net.FlagUp) != 0 {
  105. addrs, _ := netInterfaces[i].Addrs()
  106. for _, address := range addrs {
  107. if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
  108. if ipnet.IP.To4() != nil {
  109. ipv4 += ipnet.IP.String() + " "
  110. } else if ipnet.IP.To16() != nil && !ipnet.IP.IsLinkLocalUnicast() {
  111. ipv6 += ipnet.IP.String() + " "
  112. }
  113. }
  114. }
  115. }
  116. }
  117. info += t.I18nBot("tgbot.messages.ipv4", "IPv4=="+ipv4)
  118. info += t.I18nBot("tgbot.messages.ipv6", "IPv6=="+ipv6)
  119. }
  120. info += t.I18nBot("tgbot.messages.serverUpTime", "UpTime=="+strconv.FormatUint(t.lastStatus.Uptime/86400, 10), "Unit=="+t.I18nBot("tgbot.days"))
  121. 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))
  122. info += t.I18nBot("tgbot.messages.serverMemory", "Current=="+common.FormatTraffic(int64(t.lastStatus.Mem.Current)), "Total=="+common.FormatTraffic(int64(t.lastStatus.Mem.Total)))
  123. info += t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(len(onlines)))
  124. info += t.I18nBot("tgbot.messages.tcpCount", "Count=="+strconv.Itoa(t.lastStatus.TcpCount))
  125. info += t.I18nBot("tgbot.messages.udpCount", "Count=="+strconv.Itoa(t.lastStatus.UdpCount))
  126. 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)))
  127. info += t.I18nBot("tgbot.messages.xrayStatus", "State=="+fmt.Sprint(t.lastStatus.Xray.State))
  128. // Cache the complete server stats
  129. t.setCachedServerStats(info)
  130. return info
  131. }
  132. // UserLoginNotify publishes a login event to the event bus.
  133. func (t *Tgbot) UserLoginNotify(attempt LoginAttempt) {
  134. if attempt.Username == "" || attempt.IP == "" || attempt.Time == "" {
  135. logger.Warning("UserLoginNotify failed, invalid info!")
  136. return
  137. }
  138. if EventBus == nil {
  139. return
  140. }
  141. status := "fail"
  142. if attempt.Status == LoginSuccess {
  143. status = "success"
  144. }
  145. EventBus.Publish(eventbus.Event{
  146. Type: eventbus.EventLoginAttempt,
  147. Source: attempt.IP,
  148. Data: &eventbus.LoginEventData{
  149. Username: attempt.Username,
  150. IP: attempt.IP,
  151. Time: attempt.Time,
  152. Status: status,
  153. Reason: attempt.Reason,
  154. },
  155. })
  156. }
  157. // getExhausted retrieves and sends information about exhausted clients.
  158. func (t *Tgbot) getExhausted(chatId int64) {
  159. trDiff := int64(0)
  160. exDiff := int64(0)
  161. now := time.Now().Unix() * 1000
  162. var exhaustedInbounds []model.Inbound
  163. var exhaustedClients []xray.ClientTraffic
  164. var disabledInbounds []model.Inbound
  165. var disabledClients []xray.ClientTraffic
  166. TrafficThreshold, err := t.settingService.GetTrafficDiff()
  167. if err == nil && TrafficThreshold > 0 {
  168. trDiff = int64(TrafficThreshold) * 1073741824
  169. }
  170. ExpireThreshold, err := t.settingService.GetExpireDiff()
  171. if err == nil && ExpireThreshold > 0 {
  172. exDiff = int64(ExpireThreshold) * 86400000
  173. }
  174. inbounds, err := t.inboundService.GetAllInbounds()
  175. if err != nil {
  176. logger.Warning("Unable to load Inbounds", err)
  177. }
  178. seenClients := make(map[string]bool)
  179. for _, inbound := range inbounds {
  180. if inbound.Enable {
  181. if (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) ||
  182. (inbound.Total > 0 && (inbound.Total-(inbound.Up+inbound.Down) < trDiff)) {
  183. exhaustedInbounds = append(exhaustedInbounds, *inbound)
  184. }
  185. if len(inbound.ClientStats) > 0 {
  186. for _, client := range inbound.ClientStats {
  187. if seenClients[client.Email] {
  188. continue
  189. }
  190. seenClients[client.Email] = true
  191. if client.Enable {
  192. if (client.ExpiryTime > 0 && (client.ExpiryTime-now < exDiff)) ||
  193. (client.Total > 0 && (client.Total-(client.Up+client.Down) < trDiff)) {
  194. exhaustedClients = append(exhaustedClients, client)
  195. }
  196. } else {
  197. disabledClients = append(disabledClients, client)
  198. }
  199. }
  200. }
  201. } else {
  202. disabledInbounds = append(disabledInbounds, *inbound)
  203. }
  204. }
  205. // Inbounds
  206. output := ""
  207. output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.inbounds"))
  208. output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledInbounds)))
  209. output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedInbounds)))
  210. if len(exhaustedInbounds) > 0 {
  211. output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.inbounds"))
  212. for _, inbound := range exhaustedInbounds {
  213. output += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)
  214. output += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port))
  215. output += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down))
  216. if inbound.ExpiryTime == 0 {
  217. output += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited"))
  218. } else {
  219. output += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
  220. }
  221. output += "\r\n"
  222. }
  223. }
  224. // Clients
  225. exhaustedCC := len(exhaustedClients)
  226. output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients"))
  227. output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients)))
  228. output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(exhaustedCC))
  229. if exhaustedCC > 0 {
  230. output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.clients"))
  231. var buttons []telego.InlineKeyboardButton
  232. for _, traffic := range exhaustedClients {
  233. output += t.clientInfoMsg(&traffic, true, false, false, true, true, false)
  234. output += "\r\n"
  235. buttons = append(buttons, tu.InlineKeyboardButton(traffic.Email).WithCallbackData(t.encodeQuery("client_get_usage "+traffic.Email)))
  236. }
  237. cols := 0
  238. if exhaustedCC < 11 {
  239. cols = 1
  240. } else {
  241. cols = 2
  242. }
  243. output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
  244. keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))
  245. t.SendMsgToTgbot(chatId, output, keyboard)
  246. } else {
  247. output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
  248. t.SendMsgToTgbot(chatId, output)
  249. }
  250. }
  251. // notifyExhausted sends notifications for exhausted clients.
  252. func (t *Tgbot) notifyExhausted() {
  253. trDiff := int64(0)
  254. exDiff := int64(0)
  255. now := time.Now().Unix() * 1000
  256. TrafficThreshold, err := t.settingService.GetTrafficDiff()
  257. if err == nil && TrafficThreshold > 0 {
  258. trDiff = int64(TrafficThreshold) * 1073741824
  259. }
  260. ExpireThreshold, err := t.settingService.GetExpireDiff()
  261. if err == nil && ExpireThreshold > 0 {
  262. exDiff = int64(ExpireThreshold) * 86400000
  263. }
  264. inbounds, err := t.inboundService.GetAllInbounds()
  265. if err != nil {
  266. logger.Warning("Unable to load Inbounds", err)
  267. }
  268. var chatIDsDone []int64
  269. for _, inbound := range inbounds {
  270. if inbound.Enable {
  271. if len(inbound.ClientStats) > 0 {
  272. clients, err := t.inboundService.GetClients(inbound)
  273. if err == nil {
  274. for _, client := range clients {
  275. if client.TgID != 0 {
  276. chatID := client.TgID
  277. if !int64Contains(chatIDsDone, chatID) && !checkAdmin(chatID) {
  278. var disabledClients []xray.ClientTraffic
  279. var exhaustedClients []xray.ClientTraffic
  280. traffics, err := t.inboundService.GetClientTrafficTgBot(client.TgID)
  281. if err == nil && len(traffics) > 0 {
  282. var output strings.Builder
  283. output.WriteString(t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients")))
  284. for _, traffic := range traffics {
  285. if traffic.Enable {
  286. if (traffic.ExpiryTime > 0 && (traffic.ExpiryTime-now < exDiff)) ||
  287. (traffic.Total > 0 && (traffic.Total-(traffic.Up+traffic.Down) < trDiff)) {
  288. exhaustedClients = append(exhaustedClients, *traffic)
  289. }
  290. } else {
  291. disabledClients = append(disabledClients, *traffic)
  292. }
  293. }
  294. if len(exhaustedClients) > 0 {
  295. output.WriteString(t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients))))
  296. if len(disabledClients) > 0 {
  297. output.WriteString(t.I18nBot("tgbot.clients"))
  298. output.WriteString(":\r\n")
  299. for _, traffic := range disabledClients {
  300. output.WriteString(" ")
  301. output.WriteString(traffic.Email)
  302. }
  303. output.WriteString("\r\n")
  304. }
  305. output.WriteString("\r\n")
  306. output.WriteString(t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedClients))))
  307. for _, traffic := range exhaustedClients {
  308. output.WriteString(t.clientInfoMsg(&traffic, true, false, false, true, true, false))
  309. output.WriteString("\r\n")
  310. }
  311. t.SendMsgToTgbot(chatID, output.String())
  312. }
  313. chatIDsDone = append(chatIDsDone, chatID)
  314. }
  315. }
  316. }
  317. }
  318. }
  319. }
  320. }
  321. }
  322. }
  323. // onlineClients retrieves and sends information about online clients.
  324. func (t *Tgbot) onlineClients(chatId int64, messageID ...int) {
  325. if !service.XrayProcess().IsRunning() {
  326. return
  327. }
  328. onlines := service.XrayProcess().GetOnlineClients()
  329. onlinesCount := len(onlines)
  330. output := t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(onlinesCount))
  331. keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow(
  332. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("onlines_refresh"))))
  333. if onlinesCount > 0 {
  334. var buttons []telego.InlineKeyboardButton
  335. for _, online := range onlines {
  336. buttons = append(buttons, tu.InlineKeyboardButton(online).WithCallbackData(t.encodeQuery("client_get_usage "+online)))
  337. }
  338. cols := 0
  339. if onlinesCount < 21 {
  340. cols = 2
  341. } else if onlinesCount < 61 {
  342. cols = 3
  343. } else {
  344. cols = 4
  345. }
  346. keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, tu.InlineKeyboardCols(cols, buttons...)...)
  347. }
  348. if len(messageID) > 0 {
  349. t.editMessageTgBot(chatId, messageID[0], output, keyboard)
  350. } else {
  351. t.SendMsgToTgbot(chatId, output, keyboard)
  352. }
  353. }
  354. // sendBackup sends a backup of the database and configuration files.
  355. func (t *Tgbot) sendBackup(chatId int64) {
  356. output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
  357. t.SendMsgToTgbot(chatId, output)
  358. // Send database backup (SQLite file, or a pg_dump archive on PostgreSQL)
  359. dbData, err := t.serverService.GetDb()
  360. if err == nil {
  361. dbFilename := "x-ui.db"
  362. if database.IsPostgres() {
  363. dbFilename = "x-ui.dump"
  364. }
  365. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  366. document := tu.Document(
  367. tu.ID(chatId),
  368. tu.FileFromBytes(dbData, dbFilename),
  369. )
  370. _, err = bot.SendDocument(ctx, document)
  371. cancel()
  372. if err != nil {
  373. logger.Error("Error in uploading backup: ", err)
  374. }
  375. } else {
  376. logger.Error("Error in getting db backup: ", err)
  377. }
  378. // Small delay between file sends
  379. time.Sleep(500 * time.Millisecond)
  380. // Send config.json backup
  381. file, err := os.Open(xray.GetConfigPath())
  382. if err == nil {
  383. defer file.Close()
  384. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  385. defer cancel()
  386. document := tu.Document(
  387. tu.ID(chatId),
  388. tu.File(file),
  389. )
  390. _, err = bot.SendDocument(ctx, document)
  391. if err != nil {
  392. logger.Error("Error in uploading config.json: ", err)
  393. }
  394. } else {
  395. logger.Error("Error in opening config.json file for backup: ", err)
  396. }
  397. }
  398. // sendBanLogs sends the ban logs to the specified chat.
  399. func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
  400. if dt {
  401. output := t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05"))
  402. t.SendMsgToTgbot(chatId, output)
  403. }
  404. file, err := os.Open(xray.GetIPLimitBannedPrevLogPath())
  405. if err == nil {
  406. // Check if the file is non-empty before attempting to upload
  407. fileInfo, _ := file.Stat()
  408. if fileInfo.Size() > 0 {
  409. document := tu.Document(
  410. tu.ID(chatId),
  411. tu.File(file),
  412. )
  413. _, err = bot.SendDocument(context.Background(), document)
  414. if err != nil {
  415. logger.Error("Error in uploading IPLimitBannedPrevLog: ", err)
  416. }
  417. } else {
  418. logger.Warning("IPLimitBannedPrevLog file is empty, not uploading.")
  419. }
  420. file.Close()
  421. } else {
  422. logger.Error("Error in opening IPLimitBannedPrevLog file for backup: ", err)
  423. }
  424. file, err = os.Open(xray.GetIPLimitBannedLogPath())
  425. if err == nil {
  426. // Check if the file is non-empty before attempting to upload
  427. fileInfo, _ := file.Stat()
  428. if fileInfo.Size() > 0 {
  429. document := tu.Document(
  430. tu.ID(chatId),
  431. tu.File(file),
  432. )
  433. _, err = bot.SendDocument(context.Background(), document)
  434. if err != nil {
  435. logger.Error("Error in uploading IPLimitBannedLog: ", err)
  436. }
  437. } else {
  438. logger.Warning("IPLimitBannedLog file is empty, not uploading.")
  439. }
  440. file.Close()
  441. } else {
  442. logger.Error("Error in opening IPLimitBannedLog file for backup: ", err)
  443. }
  444. }