package tgbot import ( "context" "fmt" "net" "os" "strconv" "strings" "time" "github.com/mhsanaei/3x-ui/v3/internal/config" "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/mhsanaei/3x-ui/v3/internal/util/common" "github.com/mhsanaei/3x-ui/v3/internal/web/service" "github.com/mhsanaei/3x-ui/v3/internal/xray" "github.com/mymmrac/telego" tu "github.com/mymmrac/telego/telegoutil" ) // SendReport sends a periodic report to admin chats. func (t *Tgbot) SendReport() { runTime, err := t.settingService.GetTgbotRuntime() if err == nil && len(runTime) > 0 { msg := "" msg += t.I18nBot("tgbot.messages.report", "RunTime=="+runTime) msg += t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05")) t.SendMsgToTgbotAdmins(msg) } info := t.sendServerUsage() t.SendMsgToTgbotAdmins(info) t.sendExhaustedToAdmins() t.notifyExhausted() backupEnable, err := t.settingService.GetTgBotBackup() if err == nil && backupEnable { t.SendBackupToAdmins() } } // SendBackupToAdmins sends a database backup to admin chats. func (t *Tgbot) SendBackupToAdmins() { if !t.IsRunning() { return } for i, adminId := range adminIds { t.sendBackup(int64(adminId)) // Add delay between sends to avoid Telegram rate limits if i < len(adminIds)-1 { time.Sleep(1 * time.Second) } } } // sendExhaustedToAdmins sends notifications about exhausted clients to admins. func (t *Tgbot) sendExhaustedToAdmins() { if !t.IsRunning() { return } for _, adminId := range adminIds { t.getExhausted(int64(adminId)) } } // getServerUsage retrieves and formats server usage information. func (t *Tgbot) getServerUsage(chatId int64, messageID ...int) string { info := t.prepareServerUsageInfo() keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("usage_refresh")))) if len(messageID) > 0 { t.editMessageTgBot(chatId, messageID[0], info, keyboard) } else { t.SendMsgToTgbot(chatId, info, keyboard) } return info } // Send server usage without an inline keyboard func (t *Tgbot) sendServerUsage() string { info := t.prepareServerUsageInfo() return info } // prepareServerUsageInfo prepares the server usage information string. func (t *Tgbot) prepareServerUsageInfo() string { // Check if we have cached data first if cachedStats, found := t.getCachedServerStats(); found { return cachedStats } info, ipv4, ipv6 := "", "", "" // get latest status of server with caching if cachedStatus, found := t.getCachedStatus(); found { t.lastStatus = cachedStatus } else { t.lastStatus = t.serverService.GetStatus(t.lastStatus) t.setCachedStatus(t.lastStatus) } onlines := service.XrayProcess().GetOnlineClients() info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) info += t.I18nBot("tgbot.messages.version", "Version=="+config.GetVersion()) info += t.I18nBot("tgbot.messages.xrayVersion", "XrayVersion=="+fmt.Sprint(t.lastStatus.Xray.Version)) // get ip address netInterfaces, err := net.Interfaces() if err != nil { logger.Error("net.Interfaces failed, err: ", err.Error()) info += t.I18nBot("tgbot.messages.ip", "IP=="+t.I18nBot("tgbot.unknown")) info += "\r\n" } else { for i := range netInterfaces { if (netInterfaces[i].Flags & net.FlagUp) != 0 { addrs, _ := netInterfaces[i].Addrs() for _, address := range addrs { if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { if ipnet.IP.To4() != nil { ipv4 += ipnet.IP.String() + " " } else if ipnet.IP.To16() != nil && !ipnet.IP.IsLinkLocalUnicast() { ipv6 += ipnet.IP.String() + " " } } } } } info += t.I18nBot("tgbot.messages.ipv4", "IPv4=="+ipv4) info += t.I18nBot("tgbot.messages.ipv6", "IPv6=="+ipv6) } info += t.I18nBot("tgbot.messages.serverUpTime", "UpTime=="+strconv.FormatUint(t.lastStatus.Uptime/86400, 10), "Unit=="+t.I18nBot("tgbot.days")) 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)) info += t.I18nBot("tgbot.messages.serverMemory", "Current=="+common.FormatTraffic(int64(t.lastStatus.Mem.Current)), "Total=="+common.FormatTraffic(int64(t.lastStatus.Mem.Total))) info += t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(len(onlines))) info += t.I18nBot("tgbot.messages.tcpCount", "Count=="+strconv.Itoa(t.lastStatus.TcpCount)) info += t.I18nBot("tgbot.messages.udpCount", "Count=="+strconv.Itoa(t.lastStatus.UdpCount)) 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))) info += t.I18nBot("tgbot.messages.xrayStatus", "State=="+fmt.Sprint(t.lastStatus.Xray.State)) // Cache the complete server stats t.setCachedServerStats(info) return info } // UserLoginNotify sends a notification about user login attempts to admins. func (t *Tgbot) UserLoginNotify(attempt LoginAttempt) { if !t.IsRunning() { return } if attempt.Username == "" || attempt.IP == "" || attempt.Time == "" { logger.Warning("UserLoginNotify failed, invalid info!") return } loginNotifyEnabled, err := t.settingService.GetTgBotLoginNotify() if err != nil || !loginNotifyEnabled { return } msg := "" switch attempt.Status { case LoginSuccess: msg += t.I18nBot("tgbot.messages.loginSuccess") msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) case LoginFail: msg += t.I18nBot("tgbot.messages.loginFailed") msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) if attempt.Reason != "" { msg += t.I18nBot("tgbot.messages.reason", "Reason=="+attempt.Reason) } } msg += t.I18nBot("tgbot.messages.username", "Username=="+attempt.Username) msg += t.I18nBot("tgbot.messages.ip", "IP=="+attempt.IP) msg += t.I18nBot("tgbot.messages.time", "Time=="+attempt.Time) go t.SendMsgToTgbotAdmins(msg) } // getExhausted retrieves and sends information about exhausted clients. func (t *Tgbot) getExhausted(chatId int64) { trDiff := int64(0) exDiff := int64(0) now := time.Now().Unix() * 1000 var exhaustedInbounds []model.Inbound var exhaustedClients []xray.ClientTraffic var disabledInbounds []model.Inbound var disabledClients []xray.ClientTraffic TrafficThreshold, err := t.settingService.GetTrafficDiff() if err == nil && TrafficThreshold > 0 { trDiff = int64(TrafficThreshold) * 1073741824 } ExpireThreshold, err := t.settingService.GetExpireDiff() if err == nil && ExpireThreshold > 0 { exDiff = int64(ExpireThreshold) * 86400000 } inbounds, err := t.inboundService.GetAllInbounds() if err != nil { logger.Warning("Unable to load Inbounds", err) } for _, inbound := range inbounds { if inbound.Enable { if (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) || (inbound.Total > 0 && (inbound.Total-(inbound.Up+inbound.Down) < trDiff)) { exhaustedInbounds = append(exhaustedInbounds, *inbound) } if len(inbound.ClientStats) > 0 { for _, client := range inbound.ClientStats { if client.Enable { if (client.ExpiryTime > 0 && (client.ExpiryTime-now < exDiff)) || (client.Total > 0 && (client.Total-(client.Up+client.Down) < trDiff)) { exhaustedClients = append(exhaustedClients, client) } } else { disabledClients = append(disabledClients, client) } } } } else { disabledInbounds = append(disabledInbounds, *inbound) } } // Inbounds output := "" output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.inbounds")) output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledInbounds))) output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedInbounds))) if len(exhaustedInbounds) > 0 { output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.inbounds")) for _, inbound := range exhaustedInbounds { output += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark) output += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)) output += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)) if inbound.ExpiryTime == 0 { output += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited")) } else { output += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) } output += "\r\n" } } // Clients exhaustedCC := len(exhaustedClients) output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients")) output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients))) output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(exhaustedCC)) if exhaustedCC > 0 { output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.clients")) var buttons []telego.InlineKeyboardButton for _, traffic := range exhaustedClients { output += t.clientInfoMsg(&traffic, true, false, false, true, true, false) output += "\r\n" buttons = append(buttons, tu.InlineKeyboardButton(traffic.Email).WithCallbackData(t.encodeQuery("client_get_usage "+traffic.Email))) } cols := 0 if exhaustedCC < 11 { cols = 1 } else { cols = 2 } output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...)) t.SendMsgToTgbot(chatId, output, keyboard) } else { output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) t.SendMsgToTgbot(chatId, output) } } // notifyExhausted sends notifications for exhausted clients. func (t *Tgbot) notifyExhausted() { trDiff := int64(0) exDiff := int64(0) now := time.Now().Unix() * 1000 TrafficThreshold, err := t.settingService.GetTrafficDiff() if err == nil && TrafficThreshold > 0 { trDiff = int64(TrafficThreshold) * 1073741824 } ExpireThreshold, err := t.settingService.GetExpireDiff() if err == nil && ExpireThreshold > 0 { exDiff = int64(ExpireThreshold) * 86400000 } inbounds, err := t.inboundService.GetAllInbounds() if err != nil { logger.Warning("Unable to load Inbounds", err) } var chatIDsDone []int64 for _, inbound := range inbounds { if inbound.Enable { if len(inbound.ClientStats) > 0 { clients, err := t.inboundService.GetClients(inbound) if err == nil { for _, client := range clients { if client.TgID != 0 { chatID := client.TgID if !int64Contains(chatIDsDone, chatID) && !checkAdmin(chatID) { var disabledClients []xray.ClientTraffic var exhaustedClients []xray.ClientTraffic traffics, err := t.inboundService.GetClientTrafficTgBot(client.TgID) if err == nil && len(traffics) > 0 { var output strings.Builder output.WriteString(t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients"))) for _, traffic := range traffics { if traffic.Enable { if (traffic.ExpiryTime > 0 && (traffic.ExpiryTime-now < exDiff)) || (traffic.Total > 0 && (traffic.Total-(traffic.Up+traffic.Down) < trDiff)) { exhaustedClients = append(exhaustedClients, *traffic) } } else { disabledClients = append(disabledClients, *traffic) } } if len(exhaustedClients) > 0 { output.WriteString(t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients)))) if len(disabledClients) > 0 { output.WriteString(t.I18nBot("tgbot.clients")) output.WriteString(":\r\n") for _, traffic := range disabledClients { output.WriteString(" ") output.WriteString(traffic.Email) } output.WriteString("\r\n") } output.WriteString("\r\n") output.WriteString(t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedClients)))) for _, traffic := range exhaustedClients { output.WriteString(t.clientInfoMsg(&traffic, true, false, false, true, true, false)) output.WriteString("\r\n") } t.SendMsgToTgbot(chatID, output.String()) } chatIDsDone = append(chatIDsDone, chatID) } } } } } } } } } // onlineClients retrieves and sends information about online clients. func (t *Tgbot) onlineClients(chatId int64, messageID ...int) { if !service.XrayProcess().IsRunning() { return } onlines := service.XrayProcess().GetOnlineClients() onlinesCount := len(onlines) output := t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(onlinesCount)) keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("onlines_refresh")))) if onlinesCount > 0 { var buttons []telego.InlineKeyboardButton for _, online := range onlines { buttons = append(buttons, tu.InlineKeyboardButton(online).WithCallbackData(t.encodeQuery("client_get_usage "+online))) } cols := 0 if onlinesCount < 21 { cols = 2 } else if onlinesCount < 61 { cols = 3 } else { cols = 4 } keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, tu.InlineKeyboardCols(cols, buttons...)...) } if len(messageID) > 0 { t.editMessageTgBot(chatId, messageID[0], output, keyboard) } else { t.SendMsgToTgbot(chatId, output, keyboard) } } // sendBackup sends a backup of the database and configuration files. func (t *Tgbot) sendBackup(chatId int64) { output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05")) t.SendMsgToTgbot(chatId, output) // Send database backup (SQLite file, or a pg_dump archive on PostgreSQL) dbData, err := t.serverService.GetDb() if err == nil { dbFilename := "x-ui.db" if database.IsPostgres() { dbFilename = "x-ui.dump" } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) document := tu.Document( tu.ID(chatId), tu.FileFromBytes(dbData, dbFilename), ) _, err = bot.SendDocument(ctx, document) cancel() if err != nil { logger.Error("Error in uploading backup: ", err) } } else { logger.Error("Error in getting db backup: ", err) } // Small delay between file sends time.Sleep(500 * time.Millisecond) // Send config.json backup file, err := os.Open(xray.GetConfigPath()) if err == nil { defer file.Close() ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() document := tu.Document( tu.ID(chatId), tu.File(file), ) _, err = bot.SendDocument(ctx, document) if err != nil { logger.Error("Error in uploading config.json: ", err) } } else { logger.Error("Error in opening config.json file for backup: ", err) } } // sendBanLogs sends the ban logs to the specified chat. func (t *Tgbot) sendBanLogs(chatId int64, dt bool) { if dt { output := t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05")) t.SendMsgToTgbot(chatId, output) } file, err := os.Open(xray.GetIPLimitBannedPrevLogPath()) if err == nil { // Check if the file is non-empty before attempting to upload fileInfo, _ := file.Stat() if fileInfo.Size() > 0 { document := tu.Document( tu.ID(chatId), tu.File(file), ) _, err = bot.SendDocument(context.Background(), document) if err != nil { logger.Error("Error in uploading IPLimitBannedPrevLog: ", err) } } else { logger.Warning("IPLimitBannedPrevLog file is empty, not uploading.") } file.Close() } else { logger.Error("Error in opening IPLimitBannedPrevLog file for backup: ", err) } file, err = os.Open(xray.GetIPLimitBannedLogPath()) if err == nil { // Check if the file is non-empty before attempting to upload fileInfo, _ := file.Stat() if fileInfo.Size() > 0 { document := tu.Document( tu.ID(chatId), tu.File(file), ) _, err = bot.SendDocument(context.Background(), document) if err != nil { logger.Error("Error in uploading IPLimitBannedLog: ", err) } } else { logger.Warning("IPLimitBannedLog file is empty, not uploading.") } file.Close() } else { logger.Error("Error in opening IPLimitBannedLog file for backup: ", err) } }