| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- package discord
- 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/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"
- )
- // ServerProvider abstracts server status and database backup operations.
- type ServerProvider interface {
- GetStatus(lastStatus *service.Status) *service.Status
- GetDb() ([]byte, error)
- BackupFilename(requestHost string) string
- }
- // InboundProvider abstracts inbound management operations.
- type InboundProvider interface {
- GetAllInbounds() ([]*model.Inbound, error)
- }
- // BuildReport constructs the status report payload and backup attachments.
- func (s *DiscordService) BuildReport(ctx context.Context, server ServerProvider, inbound InboundProvider) (MessagePayload, []FileAttachment, error) {
- hostname, _ := os.Hostname()
- if hostname == "" {
- hostname = "3x-ui"
- }
- var status *service.Status
- if server != nil {
- status = server.GetStatus(nil)
- }
- if status == nil {
- status = &service.Status{
- Loads: []float64{0, 0, 0},
- }
- status.Xray.State = service.ProcessState("unknown")
- status.Xray.Version = "unknown"
- }
- var onlines []string
- if process := service.XrayProcess(); process != nil && process.IsRunning() {
- onlines = process.GetOnlineClients()
- }
- trDiff := int64(0)
- exDiff := int64(0)
- now := time.Now().Unix() * 1000
- trafficThreshold, err := s.settingService.GetTrafficDiff()
- if err == nil && trafficThreshold > 0 {
- trDiff = int64(trafficThreshold) * 1073741824
- }
- expireThreshold, err := s.settingService.GetExpireDiff()
- if err == nil && expireThreshold > 0 {
- exDiff = int64(expireThreshold) * 86400000
- }
- var totalInbounds, disabledInbounds, exhaustedInbounds int
- var totalClients, disabledClients, exhaustedClients int
- seenClients := make(map[string]bool)
- if inbound != nil {
- inbounds, err := inbound.GetAllInbounds()
- if err != nil {
- logger.Warning("Discord report: unable to load inbounds: ", err)
- } else {
- totalInbounds = len(inbounds)
- for _, in := range inbounds {
- if !in.Enable {
- disabledInbounds++
- } else if (in.ExpiryTime > 0 && (in.ExpiryTime-now < exDiff)) ||
- (in.Total > 0 && (in.Total-(in.Up+in.Down) < trDiff)) {
- exhaustedInbounds++
- }
- for _, client := range in.ClientStats {
- if seenClients[client.Email] {
- continue
- }
- seenClients[client.Email] = true
- totalClients++
- if !client.Enable {
- disabledClients++
- } else if (client.ExpiryTime > 0 && (client.ExpiryTime-now < exDiff)) ||
- (client.Total > 0 && (client.Total-(client.Up+client.Down) < trDiff)) {
- exhaustedClients++
- }
- }
- }
- }
- }
- tr := translator(s.settingService)
- days := status.Uptime / 86400
- hours := (status.Uptime % 86400) / 3600
- uptimeStr := tr("discord.values.uptime", "Days=="+fmt.Sprint(days), "Hours=="+fmt.Sprint(hours))
- ramStr := fmt.Sprintf("%s / %s", common.FormatTraffic(int64(status.Mem.Current)), common.FormatTraffic(int64(status.Mem.Total)))
- trafficStr := tr("discord.values.traffic",
- "Up=="+common.FormatTraffic(int64(status.NetTraffic.Sent)),
- "Down=="+common.FormatTraffic(int64(status.NetTraffic.Recv)),
- "Total=="+common.FormatTraffic(int64(status.NetTraffic.Sent+status.NetTraffic.Recv)),
- )
- load1, load2, load3 := 0.0, 0.0, 0.0
- if len(status.Loads) > 0 {
- load1 = status.Loads[0]
- }
- if len(status.Loads) > 1 {
- load2 = status.Loads[1]
- }
- if len(status.Loads) > 2 {
- load3 = status.Loads[2]
- }
- loadStr := fmt.Sprintf("%.2f, %.2f, %.2f", load1, load2, load3)
- fields := []EmbedField{
- {Name: tr("host"), Value: hostname, Inline: true},
- {Name: tr("discord.fields.panelVersion"), Value: config.GetPanelVersion(), Inline: true},
- {Name: tr("discord.fields.xrayCore"), Value: fmt.Sprintf("%s (%s)", status.Xray.Version, status.Xray.State), Inline: true},
- {Name: tr("pages.index.uptime"), Value: uptimeStr, Inline: true},
- {Name: tr("discord.fields.systemLoad"), Value: loadStr, Inline: true},
- {Name: tr("pages.index.memory"), Value: ramStr, Inline: true},
- {Name: tr("discord.fields.networkTraffic"), Value: trafficStr, Inline: false},
- {Name: tr("pages.index.historyTabConnections"), Value: fmt.Sprintf("TCP: %d | UDP: %d", status.TcpCount, status.UdpCount), Inline: true},
- {Name: tr("pages.index.historyTitleOnline"), Value: strconv.Itoa(len(onlines)), Inline: true},
- {Name: tr("tgbot.inbounds"), Value: tr("discord.values.counts", "Total=="+strconv.Itoa(totalInbounds), "Depleting=="+strconv.Itoa(exhaustedInbounds), "Disabled=="+strconv.Itoa(disabledInbounds)), Inline: false},
- {Name: tr("clients"), Value: tr("discord.values.counts", "Total=="+strconv.Itoa(totalClients), "Depleting=="+strconv.Itoa(exhaustedClients), "Disabled=="+strconv.Itoa(disabledClients)), Inline: false},
- }
- ipv4, ipv6 := getInterfaceIPs()
- if ipv4 != "" {
- fields = append(fields, EmbedField{Name: "IPv4", Value: ipv4, Inline: true})
- }
- if ipv6 != "" {
- fields = append(fields, EmbedField{Name: "IPv6", Value: ipv6, Inline: true})
- }
- runTime, _ := s.settingService.GetDiscordRunTime()
- if runTime == "" {
- runTime = "@daily"
- }
- embed := Embed{
- Title: tr("discord.report.title"),
- Description: tr("discord.report.summary", "Host=="+hostname),
- Color: ColorBlue,
- Timestamp: time.Now().UTC().Format(time.RFC3339),
- Fields: fields,
- Footer: &EmbedFooter{
- Text: tr("discord.report.footer", "RunTime=="+runTime),
- },
- }
- payload := MessagePayload{
- Embeds: []Embed{embed},
- }
- var files []FileAttachment
- backupEnabled, err := s.settingService.GetDiscordBotBackup()
- if err == nil && backupEnabled && server != nil {
- dbData, err := server.GetDb()
- if err != nil {
- logger.Warning("Discord report: failed to get DB backup: ", err)
- } else if len(dbData) > 0 {
- filename := server.BackupFilename("")
- if filename == "" {
- filename = "x-ui.db"
- }
- files = append(files, FileAttachment{
- Filename: filename,
- Data: dbData,
- })
- }
- configPath := xray.GetConfigPath()
- if configData, err := os.ReadFile(configPath); err == nil && len(configData) > 0 {
- files = append(files, FileAttachment{
- Filename: "config.json",
- Data: configData,
- })
- }
- }
- return payload, files, nil
- }
- // SendReport generates and sends the periodic report to Discord.
- func (s *DiscordService) SendReport(ctx context.Context, server ServerProvider, inbound InboundProvider) error {
- payload, files, err := s.BuildReport(ctx, server, inbound)
- if err != nil {
- return fmt.Errorf("build discord report: %w", err)
- }
- // Separate messages: a backup over Discord's upload cap must not drop the report with it.
- if err := s.SendMessage(ctx, payload); err != nil {
- return err
- }
- if len(files) == 0 {
- return nil
- }
- return s.SendMessageWithFiles(ctx, MessagePayload{}, files...)
- }
- func getInterfaceIPs() (ipv4, ipv6 string) {
- netInterfaces, err := net.Interfaces()
- if err != nil {
- return "", ""
- }
- var v4s, v6s []string
- for _, iface := range netInterfaces {
- if (iface.Flags&net.FlagUp) == 0 || (iface.Flags&net.FlagLoopback) != 0 {
- continue
- }
- addrs, err := iface.Addrs()
- if err != nil {
- continue
- }
- for _, addr := range addrs {
- ipnet, ok := addr.(*net.IPNet)
- if !ok || ipnet.IP.IsLoopback() {
- continue
- }
- if ip := ipnet.IP.To4(); ip != nil {
- v4s = append(v4s, ip.String())
- } else if ip := ipnet.IP.To16(); ip != nil && !ipnet.IP.IsLinkLocalUnicast() {
- v6s = append(v6s, ip.String())
- }
- }
- }
- return strings.Join(v4s, ", "), strings.Join(v6s, ", ")
- }
|