tgbot_report.go 17 KB

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