package report import ( "bytes" "encoding/json" "fmt" "os" "path/filepath" "regexp" "sort" "strings" "time" ) type ClickLog struct { Timestamp time.Time `json:"timestamp"` Element string `json:"element"` PageURL string `json:"pageUrl"` Text *string `json:"text"` ClassName *string `json:"className"` } type NetworkLog struct { Type string `json:"type"` Method string `json:"method"` URL string `json:"url"` PageURL string `json:"pageUrl"` Status int `json:"status"` Duration int `json:"duration"` DataPreview string `json:"dataPreview"` Body string `json:"body"` Timestamp time.Time `json:"timestamp"` } type ReactQueryLog struct { Type string `json:"type"` PageURL string `json:"pageUrl"` Status string `json:"status"` Hash string `json:"hash"` Error interface{} `json:"error"` DataPreview string `json:"dataPreview"` DataType string `json:"dataType"` Timestamp time.Time `json:"timestamp"` } type ConsoleLog struct { Type string `json:"type"` Content string `json:"content"` PageURL string `json:"pageUrl"` Timestamp time.Time `json:"timestamp"` } type UnlockedLocalFile struct { Path string `json:"path"` MediaId int `json:"mediaId"` } type IssueReport struct { CreatedAt time.Time `json:"createdAt"` UserAgent string `json:"userAgent"` AppVersion string `json:"appVersion"` OS string `json:"os"` Arch string `json:"arch"` ClickLogs []*ClickLog `json:"clickLogs,omitempty"` NetworkLogs []*NetworkLog `json:"networkLogs,omitempty"` ReactQueryLogs []*ReactQueryLog `json:"reactQueryLogs,omitempty"` ConsoleLogs []*ConsoleLog `json:"consoleLogs,omitempty"` UnlockedLocalFiles []*UnlockedLocalFile `json:"unlockedLocalFiles,omitempty"` ScanLogs []string `json:"scanLogs,omitempty"` ServerLogs string `json:"serverLogs,omitempty"` ServerStatus string `json:"status,omitempty"` } func NewIssueReport(userAgent, appVersion, _os, arch string, logsDir string, isAnimeLibraryIssue bool, serverStatus interface{}, toRedact []string) (ret *IssueReport, err error) { ret = &IssueReport{ CreatedAt: time.Now(), UserAgent: userAgent, AppVersion: appVersion, OS: _os, Arch: arch, ClickLogs: make([]*ClickLog, 0), NetworkLogs: make([]*NetworkLog, 0), ReactQueryLogs: make([]*ReactQueryLog, 0), ConsoleLogs: make([]*ConsoleLog, 0), UnlockedLocalFiles: make([]*UnlockedLocalFile, 0), ScanLogs: make([]string, 0), ServerLogs: "", ServerStatus: "", } // Get all log files in the directory entries, err := os.ReadDir(logsDir) if err != nil { return nil, fmt.Errorf("failed to read log directory: %w", err) } var serverLogFiles []os.FileInfo var scanLogFiles []os.FileInfo for _, file := range entries { if strings.HasPrefix(file.Name(), "seanime-") { info, err := file.Info() if err != nil { continue } serverLogFiles = append(serverLogFiles, info) } if strings.Contains(file.Name(), "-scan") { info, err := file.Info() if err != nil { continue } // Check if file is newer than 1 day if time.Since(info.ModTime()).Hours() < 24 { scanLogFiles = append(scanLogFiles, info) } } } userPathPattern := regexp.MustCompile(`(?i)(/home/|/Users/|C:\\Users\\)([^/\\]+)`) if serverStatus != nil { serverStatusMarshaled, err := json.Marshal(serverStatus) if err == nil { // pretty print the json var prettyJSON bytes.Buffer err = json.Indent(&prettyJSON, serverStatusMarshaled, "", " ") if err == nil { ret.ServerStatus = prettyJSON.String() for _, redact := range toRedact { ret.ServerStatus = strings.ReplaceAll(ret.ServerStatus, redact, "[REDACTED]") } ret.ServerStatus = userPathPattern.ReplaceAllString(ret.ServerStatus, "${1}[REDACTED]") } } } if len(serverLogFiles) > 0 { sort.Slice(serverLogFiles, func(i, j int) bool { return serverLogFiles[i].ModTime().After(serverLogFiles[j].ModTime()) }) // Get the most recent log file latestLog := serverLogFiles[0] latestLogPath := filepath.Join(logsDir, latestLog.Name()) latestLogContent, err := os.ReadFile(latestLogPath) if err != nil { return nil, fmt.Errorf("failed to read log file: %w", err) } latestLogContent = userPathPattern.ReplaceAll(latestLogContent, []byte("${1}[REDACTED]")) for _, redact := range toRedact { latestLogContent = bytes.ReplaceAll(latestLogContent, []byte(redact), []byte("[REDACTED]")) } ret.ServerLogs = string(latestLogContent) } if isAnimeLibraryIssue { if len(scanLogFiles) > 0 { for _, file := range scanLogFiles { scanLogPath := filepath.Join(logsDir, file.Name()) scanLogContent, err := os.ReadFile(scanLogPath) if err != nil { continue } scanLogContent = userPathPattern.ReplaceAll(scanLogContent, []byte("${1}[REDACTED]")) if len(scanLogContent) == 0 { ret.ScanLogs = append(ret.ScanLogs, "EMPTY") } else { ret.ScanLogs = append(ret.ScanLogs, string(scanLogContent)) } } } } return } func (ir *IssueReport) AddClickLogs(clickLogs []*ClickLog) { ir.ClickLogs = append(ir.ClickLogs, clickLogs...) } func (ir *IssueReport) AddNetworkLogs(networkLogs []*NetworkLog) { ir.NetworkLogs = append(ir.NetworkLogs, networkLogs...) } func (ir *IssueReport) AddReactQueryLogs(reactQueryLogs []*ReactQueryLog) { ir.ReactQueryLogs = append(ir.ReactQueryLogs, reactQueryLogs...) } func (ir *IssueReport) AddConsoleLogs(consoleLogs []*ConsoleLog) { ir.ConsoleLogs = append(ir.ConsoleLogs, consoleLogs...) }