| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493 |
- 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)
- }
- }
|