mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-10 06:36:06 +00:00
dabd3f5d2b
Name downloaded DB backups after the host shown in the panel title (c.Request.Host) when available, falling back to the configured web domain and then the public IP. Telegram-sent backups have no request context and keep the domain/IP behavior.
491 lines
16 KiB
Go
491 lines
16 KiB
Go
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/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/eventbus"
|
|
"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 publishes a login event to the event bus.
|
|
func (t *Tgbot) UserLoginNotify(attempt LoginAttempt) {
|
|
if attempt.Username == "" || attempt.IP == "" || attempt.Time == "" {
|
|
logger.Warning("UserLoginNotify failed, invalid info!")
|
|
return
|
|
}
|
|
|
|
if EventBus == nil {
|
|
return
|
|
}
|
|
|
|
status := "fail"
|
|
if attempt.Status == LoginSuccess {
|
|
status = "success"
|
|
}
|
|
|
|
EventBus.Publish(eventbus.Event{
|
|
Type: eventbus.EventLoginAttempt,
|
|
Source: attempt.IP,
|
|
Data: &eventbus.LoginEventData{
|
|
Username: attempt.Username,
|
|
IP: attempt.IP,
|
|
Time: attempt.Time,
|
|
Status: status,
|
|
Reason: attempt.Reason,
|
|
},
|
|
})
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
seenClients := make(map[string]bool)
|
|
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 seenClients[client.Email] {
|
|
continue
|
|
}
|
|
seenClients[client.Email] = true
|
|
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 := t.serverService.BackupFilename("")
|
|
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)
|
|
}
|
|
}
|