using System.Collections.Generic;using System.Globalization;using System.IO.Compression;using System.Runtime.InteropServices;using System.Text.RegularExpressions; static class ProblemReport const long PictureBudget = 8L * 1024 * 1024, LogTail = 2L * 1024 * 1024, OldLogTail = 1L * 1024 * 1024; const long LinuxTail = 256L * 1024; public static string Write(string appDir, string toDir, string roblox) Directory.CreateDirectory(toDir); string stamp = DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture); string zip = Path.Combine(toDir, "IdleMafiaBot-report-" + stamp + ".zip"); for (int n = 2; File.Exists(zip); n++) zip = Path.Combine(toDir, "IdleMafiaBot-report-" + stamp + "-" + n + ".zip"); var skipped = new List<string>(); var leftOut = new List<string>(); using (var z = new ZipArchive(new FileStream(zip, FileMode.CreateNew), ZipArchiveMode.Create)) AddText(z, Path.Combine(appDir, "IdleMafiaBot.log"), "IdleMafiaBot.log", LogTail, skipped, leftOut); AddText(z, Path.Combine(appDir, "IdleMafiaBot.1.log"), "IdleMafiaBot.1.log", OldLogTail, skipped, leftOut); AddText(z, Path.Combine(appDir, "jobs.txt"), "jobs.txt", 0, skipped, leftOut); AddSettings(z, Path.Combine(appDir, "IdleMafiaBot.ini"), skipped); foreach (var file in Directory.GetFiles(appDir, "profile-*.txt").OrderBy(f => f, StringComparer.OrdinalIgnoreCase)) AddText(z, file, Path.GetFileName(file), 0, skipped, leftOut); AddProblems(z, Path.Combine(appDir, "problems"), skipped, leftOut); string up = Path.GetDirectoryName(appDir.TrimEnd('/', '\\')); if (!string.IsNullOrEmpty(up)) AddText(z, Path.Combine(up, "keep.log"), "keep.log", LinuxTail, skipped, leftOut); AddText(z, Path.Combine(up, "bot.out"), "bot.out", LinuxTail, skipped, leftOut); using (var w = new StreamWriter(z.CreateEntry("system.txt").Open(), new UTF8Encoding(false))) w.Write(NoUser(SystemText(roblox, skipped, leftOut))); try { File.Delete(zip); } catch (Exception) { } public static int MoveReported(string appDir, DateTime before) string active = Path.Combine(appDir, "problems", "active"), reported = Path.Combine(appDir, "problems", "reported"); if (!Directory.Exists(active)) return 0; foreach (var file in Directory.GetFiles(active, "*", SearchOption.AllDirectories)) if (File.GetLastWriteTime(file) >= before) continue; string rel = file.Substring(active.Length + 1), to = Path.Combine(reported, rel); Directory.CreateDirectory(Path.GetDirectoryName(to)); for (int n = 2; File.Exists(to); n++) to = Path.Combine(Path.GetDirectoryName(to), Path.GetFileNameWithoutExtension(rel) + "-" + n + Path.GetExtension(rel)); if (Path.GetDirectoryName(rel).Length == 0 && rel.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) moved++; static void AddProblems(ZipArchive z, string problems, List<string> skipped, List<string> leftOut) string active = Path.Combine(problems, "active"), reported = Path.Combine(problems, "reported"); if (Directory.Exists(Path.Combine(problems, "fixed"))) leftOut.Add("problems/fixed (pictures of things already fixed)"); var files = new List<string>(); var older = new List<string>(); if (Directory.Exists(active)) files.AddRange(Directory.GetFiles(active, "*", SearchOption.AllDirectories).OrderByDescending(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase)); if (Directory.Exists(reported)) var all = Directory.GetFiles(reported, "*", SearchOption.AllDirectories); var since = DateTime.Now.AddDays(-1); var recent = all.Where(f => File.GetLastWriteTime(f) >= since).OrderByDescending(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase).ToList(); older = all.Except(recent).OrderByDescending(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase).ToList(); if (older.Count > 0) leftOut.Add("problems/reported older than a day (pictures sent in an earlier report; of the last week the newest of each kind not seen since went in again, room allowing)"); if (files.Count == 0 && older.Count == 0) return; var firsts = files.GroupBy(Kind, StringComparer.OrdinalIgnoreCase).Select(g => g.First()).ToList(); var kinds = new HashSet<string>(files.Select(Kind), StringComparer.OrdinalIgnoreCase); var week = DateTime.Now.AddDays(-7); var again = new HashSet<string>(older.Where(f => File.GetLastWriteTime(f) >= week && !kinds.Contains(Kind(f))) .GroupBy(Kind, StringComparer.OrdinalIgnoreCase).Select(g => g.First())); var order = firsts.Concat(again).Concat(files.Except(firsts)).ToList(); var over = new List<string>(); foreach (var file in order) bool sent = again.Contains(file); string name = NoUser("problems/" + file.Substring(problems.Length + 1).Replace('\\', '/'), true); if (!file.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) { AddFile(z, file, name, skipped); continue; } if (IsCovered(file)) { covered++; continue; } if (used >= PictureBudget) { if (!sent) over.Add(name); continue; } try { jpg = Discord.JpgOf(file); } catch (Exception ex) { skipped.Add(name + ": " + ex.Message.Replace(file, Path.GetFileName(file))); continue; } if (jpg == null) continue; if (used > 0 && used + jpg.Length > PictureBudget) { if (!sent) { over.Add(name); used = PictureBudget; } continue; } var e = z.CreateEntry(Path.ChangeExtension(name, ".jpg"), CompressionLevel.NoCompression); using (var dst = e.Open()) dst.Write(jpg, 0, jpg.Length); if (covered > 0) leftOut.Add(covered + " problem pictures taken with another window over Roblox (they could show it)"); leftOut.Add(over.Count + " older problem pictures (to keep the zip small):"); foreach (var n in over.Take(100)) leftOut.Add(" " + n); if (over.Count > 100) leftOut.Add(" and " + (over.Count - 100) + " more"); static string Kind(string file) return Regex.Replace(Regex.Replace(Path.GetFileNameWithoutExtension(file), @"^\d{8}-\d{6}-", ""), @"-\d+$", ""); internal static bool IsCovered(string file) return Regex.IsMatch(Path.GetFileNameWithoutExtension(file), Regex.Escape(Game.Covered) + @"(-\d+)?$", RegexOptions.IgnoreCase); static void AddText(ZipArchive z, string file, string name, long tail, List<string> skipped, List<string> leftOut) if (!File.Exists(file)) return; using (var src = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) long from = tail > 0 && src.Length > tail ? src.Length - tail : 0; src.Seek(from, SeekOrigin.Begin); using (var ms = new MemoryStream()) { src.CopyTo(ms); bytes = ms.ToArray(); } int nl = Array.IndexOf(bytes, (byte)'\n'); if (nl >= 0) bytes = bytes.Skip(nl + 1).ToArray(); leftOut.Add(name + ": its first " + Size(from + (nl + 1)) + " (older lines)"); string text = NoUser(HideLink(new UTF8Encoding(false).GetString(bytes))); var e = z.CreateEntry(name, CompressionLevel.Optimal); using (var w = new StreamWriter(e.Open(), new UTF8Encoding(false))) w.Write(text); catch (IOException ex) { skipped.Add(name + ": " + ex.Message.Replace(file, name)); } catch (UnauthorizedAccessException ex) { skipped.Add(name + ": " + ex.Message.Replace(file, name)); } static void AddFile(ZipArchive z, string file, string name, List<string> skipped) if (!File.Exists(file)) return; using (var src = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) var e = z.CreateEntry(name, CompressionLevel.Optimal); using (var dst = e.Open()) src.CopyTo(dst); catch (IOException ex) { skipped.Add(name + ": " + ex.Message.Replace(file, Path.GetFileName(file))); } catch (UnauthorizedAccessException ex) { skipped.Add(name + ": " + ex.Message.Replace(file, Path.GetFileName(file))); } static void Stamp(ZipArchiveEntry e, string file) var time = File.GetLastWriteTime(file); if (time.Year >= 1980 && time.Year <= 2107) e.LastWriteTime = time; static string Size(long bytes) return bytes < 1048576 ? Math.Max(1, bytes / 1024) + " KB" : (bytes / 1048576.0).ToString("0.0", CultureInfo.InvariantCulture) + " MB"; internal static string NoUser(string text, bool fileName = false) if (string.IsNullOrEmpty(text)) return text ?? ""; string home = "", user = ""; try { home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? ""; user = Environment.UserName ?? ""; } home = home.TrimEnd('\\', '/'); text = Regex.Replace(text, Regex.Escape(home), "%USERPROFILE%", RegexOptions.IgnoreCase); text = Regex.Replace(text, Regex.Escape(home.Replace('\\', '/')), "%USERPROFILE%", RegexOptions.IgnoreCase); text = Regex.Replace(text, (fileName ? @"(?<=[\\/-])" : @"(?<=[\\/])") + Regex.Escape(user) + @"(?![A-Za-z0-9])", fileName ? "user" : "<user>", RegexOptions.IgnoreCase); static void AddSettings(ZipArchive z, string file, List<string> skipped) if (!File.Exists(file)) return; using (var src = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) using (var r = new StreamReader(src)) text = r.ReadToEnd(); text = NoUser(HideLink(text)); using (var w = new StreamWriter(z.CreateEntry("IdleMafiaBot.ini").Open(), new UTF8Encoding(false))) w.Write(text); catch (IOException ex) { skipped.Add("IdleMafiaBot.ini: " + ex.Message.Replace(file, "IdleMafiaBot.ini")); } catch (UnauthorizedAccessException ex) { skipped.Add("IdleMafiaBot.ini: " + ex.Message.Replace(file, "IdleMafiaBot.ini")); } internal static string HideLink(string text) text = System.Text.RegularExpressions.Regex.Replace(text, @"(?m)^([ \t]*(?:DiscordLink|StatsLink|RobloxUsers)[ \t]*=)[ \t]*[^\s][^\r\n]*", "$1(set, left out of this report)"); text = System.Text.RegularExpressions.Regex.Replace(text, @"(your Roblox name is )@\S+", "$1(left out)"); return System.Text.RegularExpressions.Regex.Replace(text, @"https?://\S*webhooks/\S+", "(a webhook link, left out)"); static string SystemText(string roblox, List<string> skipped, List<string> leftOut) var sb = new StringBuilder(); var offset = TimeZoneInfo.Local.GetUtcOffset(DateTime.Now); sb.AppendLine("Idle Mafia Bot " + Program.Version); sb.AppendLine("Report made: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) + " (UTC" + (offset < TimeSpan.Zero ? "-" : "+") + offset.ToString(@"hh\:mm") + ", the log's clock)"); sb.AppendLine("Linux: " + LinuxVersion()); sb.AppendLine("Windows: " + WindowsVersion()); sb.AppendLine(".NET: " + Environment.Version + (Environment.Is64BitProcess ? ", 64-bit" : ", 32-bit")); sb.AppendLine("Memory: " + Try(() => Native.MemoryText())); sb.AppendLine("Monitors (real pixels, Windows scale):"); var monitors = Monitors(); for (int i = 0; i < monitors.Count; i++) sb.AppendLine(" " + (i + 1) + ": " + Describe(monitors[i])); if (monitors.Count == 0) sb.AppendLine(" none found"); sb.AppendLine("Roblox window: " + roblox); string ocr = Ocr.Check(); sb.AppendLine("Text recognition (OCR): " + (ocr == null ? Ocr.Language : "not usable - " + ocr)); sb.AppendLine(" installed on this PC: " + Try(() => string.Join(", ", Windows.Media.Ocr.OcrEngine.AvailableRecognizerLanguages.Select(l => l.LanguageTag)))); sb.AppendLine(" Windows languages: " + Try(() => string.Join(", ", Windows.System.UserProfile.GlobalizationPreferences.Languages))); foreach (var s in skipped) sb.AppendLine("Couldn't add " + s); if (leftOut.Count > 0) sb.AppendLine("Left out of this zip:"); foreach (var s in leftOut) sb.AppendLine(" " + s); static string Try(Func<string> f) try { string s = f(); return s.Length > 0 ? s : "none"; } catch (Exception e) { return "? (" + e.Message + ")"; } static string LinuxVersion() string name = "Linux", kernel = "?"; var line = File.ReadAllLines("/etc/os-release").FirstOrDefault(l => l.StartsWith("PRETTY_NAME=", StringComparison.Ordinal)); if (line != null) name = line.Substring(12).Trim('"'); try { kernel = File.ReadAllText("/proc/sys/kernel/osrelease").Trim(); } catch (Exception) { } return name + " (kernel " + kernel + ")"; public static string RobloxWindowInfo(IntPtr pinned = default(IntPtr)) var w = new RobloxWindow { Pinned = pinned }; if (!w.Find()) return "not found (Roblox isn't open)"; return w.Width + "x" + w.Height + (RobloxWindow.CoveredBy != null ? ", under " + RobloxWindow.CoveredBy : ""); catch (Exception e) { return "? (" + e.Message + ")"; } static string WindowsVersion() using (var k = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion")) string name = k.GetValue("ProductName") as string ?? "Windows"; string release = k.GetValue("DisplayVersion") as string ?? k.GetValue("ReleaseId") as string; string build = k.GetValue("CurrentBuild") as string ?? "?"; object ubr = k.GetValue("UBR"); if (int.TryParse(build, out b) && b >= 22000) name = name.Replace("Windows 10", "Windows 11"); return name + (release != null ? " " + release : "") + " (build " + build + (ubr != null ? "." + ubr : "") + ")" + (Environment.Is64BitOperatingSystem ? ", 64-bit" : ", 32-bit"); catch (Exception) { return Environment.OSVersion.ToString(); } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public Native.RECT Monitor, Work; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string Device; delegate bool MonitorProc(IntPtr monitor, IntPtr dc, IntPtr rect, IntPtr data); [DllImport("user32.dll")] static extern bool EnumDisplayMonitors(IntPtr dc, IntPtr clip, MonitorProc proc, IntPtr data); [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern bool GetMonitorInfo(IntPtr monitor, ref MONITORINFOEX info); [DllImport("user32.dll")] static extern IntPtr MonitorFromWindow(IntPtr window, uint flags); [DllImport("shcore.dll")] static extern int GetDpiForMonitor(IntPtr monitor, int type, out uint dpiX, out uint dpiY); static List<IntPtr> Monitors() var list = new List<IntPtr>(); MonitorProc add = (m, dc, r, d) => { list.Add(m); return true; }; EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, add, IntPtr.Zero); static string Describe(IntPtr monitor) var mi = new MONITORINFOEX { cbSize = Marshal.SizeOf(typeof(MONITORINFOEX)) }; if (!GetMonitorInfo(monitor, ref mi)) return "?"; string scale = GetDpiForMonitor(monitor, 0, out dx, out dy) == 0 ? (dx * 100 / 96) + "%" : "?"; return string.Format(CultureInfo.InvariantCulture, "{0}x{1} at ({2},{3}), scale {4}{5}", r.Right - r.Left, r.Bottom - r.Top, r.Left, r.Top, scale, (mi.Flags & 1) != 0 ? ", main" : ""); catch (Exception e) { return "? (" + e.Message + ")"; } public static string RobloxWindowInfo(IntPtr pinned = default(IntPtr)) var w = new RobloxWindow { Pinned = pinned }; if (!w.Find()) return "not found (Roblox isn't open)"; if (w.Minimized) return "minimized"; return w.Width + "x" + w.Height + " client area, " + MonitorOf(w.Handle) + (RobloxWindow.ScreenFirst ? ", read from the screen (Windows gave empty pictures of it)" : "") + (RobloxWindow.CoveredBy != null ? ", under " + RobloxWindow.CoveredBy + " (no pictures to Discord)" : "") + "; open: " + string.Join(", ", RobloxWindow.All().Select(ClientRect)); catch (Exception e) { return "? (" + e.Message + ")"; } static string ClientRect(IntPtr window) var p = new Native.POINT(); if (!Native.GetClientRect(window, out r) || !Native.ClientToScreen(window, ref p)) return "?"; return (r.Right - r.Left) + "x" + (r.Bottom - r.Top) + " at " + p.X + "," + p.Y; internal static string MonitorOf(IntPtr window) int i = Monitors().IndexOf(MonitorFromWindow(window, 2)); return i >= 0 ? "on monitor " + (i + 1) : "on no monitor";