Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of DiscordTools v1.4.0
DiscordTools.dll
Decompiled a day agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; using UnityEngine.Networking; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("DiscordTools")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("DiscordTools")] [assembly: AssemblyCopyright("Copyright © 2026 warpalicious")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("C89145AB-DF73-440F-8C28-113526403F96")] [assembly: AssemblyFileVersion("1.4.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.4.0.0")] [module: UnverifiableCode] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } } namespace DiscordTools { internal static class BotApiClient { public static IEnumerator PostLogRoutine(ArchivedLog log) { string botApiUrl = DiscordToolsPlugin.GetBotApiUrl(); string botApiKey = DiscordToolsPlugin.GetBotApiKey(); if (string.IsNullOrWhiteSpace(botApiUrl)) { yield break; } if (string.IsNullOrWhiteSpace(botApiKey)) { string text = "Bot API key is not configured. Saved locally at " + log.RelativeLogPath; LogArchive.MarkBotUploadFailed(log, text); DiscordToolsPlugin.Log.LogWarning((object)text); yield break; } byte[] array; try { array = ReadDecompressedLog(log.LogPath); } catch (Exception ex) { LogArchive.MarkBotUploadFailed(log, ex.Message); DiscordToolsPlugin.Log.LogWarning((object)("Bot API upload failed for " + log.RelativeLogPath + ": " + ex.Message)); yield break; } string text2 = JsonMetadata(log); List<IMultipartFormSection> list = new List<IMultipartFormSection> { (IMultipartFormSection)new MultipartFormDataSection("metadata_json", text2, Encoding.UTF8, "application/json"), (IMultipartFormSection)new MultipartFormFileSection("file", array, GetDiscordFileName(log.LogPath), "text/plain") }; UnityWebRequest request = UnityWebRequest.Post(botApiUrl, list); try { request.SetRequestHeader("X-API-Key", botApiKey); request.SetRequestHeader("User-Agent", "DiscordTools/1.0"); yield return request.SendWebRequest(); if ((int)request.result != 1 || request.responseCode < 200 || request.responseCode >= 300) { string text3 = (string.IsNullOrWhiteSpace(request.error) ? ("HTTP " + request.responseCode) : (request.error + " (HTTP " + request.responseCode + ")")); LogArchive.MarkBotUploadFailed(log, text3); DiscordToolsPlugin.Log.LogWarning((object)("Bot API upload failed for " + log.RelativeLogPath + ": " + text3)); } else { DiscordToolsPlugin.Log.LogInfo((object)("Uploaded client log to bot API: " + log.RelativeLogPath)); } } finally { ((IDisposable)request)?.Dispose(); } } private static string JsonMetadata(ArchivedLog log) { return "{\"requestId\":\"" + EscapeJson(log.RequestId) + "\",\"playerId\":\"" + EscapeJson(log.PlayerId) + "\",\"playerName\":\"" + EscapeJson(log.PlayerName) + "\",\"playerFolder\":\"" + EscapeJson(log.PlayerFolder) + "\",\"reason\":\"" + EscapeJson(log.Reason) + "\",\"receivedAtUtc\":\"" + EscapeJson(log.ReceivedAtUtc.ToString("O", CultureInfo.InvariantCulture)) + "\",\"originalBytes\":" + log.OriginalBytes.ToString(CultureInfo.InvariantCulture) + ",\"compressedBytes\":" + log.CompressedBytes.ToString(CultureInfo.InvariantCulture) + ",\"sha256\":\"" + EscapeJson(log.Sha256) + "\",\"serverLogPath\":\"" + EscapeJson(log.RelativeLogPath) + "\"}"; } private static byte[] ReadDecompressedLog(string path) { using FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); using GZipStream gZipStream = new GZipStream(stream, CompressionMode.Decompress); using MemoryStream memoryStream = new MemoryStream(); gZipStream.CopyTo(memoryStream); return memoryStream.ToArray(); } private static string GetDiscordFileName(string path) { string fileName = Path.GetFileName(path); if (!fileName.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)) { return fileName; } return fileName.Substring(0, fileName.Length - 3); } private static string EscapeJson(string value) { return (value ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "\\r") .Replace("\n", "\\n"); } } internal static class ClientLogCommand { private static bool _registered; public static void Register() { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Expected O, but got Unknown //IL_0042: Expected O, but got Unknown //IL_003d: Unknown result type (might be due to invalid IL or missing references) if (!_registered) { _registered = true; new ConsoleCommand(DiscordToolsPlugin.CommandName.Value, "[playerNameOrSteamID] - request a full BepInEx log from that connected player", new ConsoleEventFailable(Execute), false, false, true, false, false, new ConsoleOptionsFetcher(GetPlayerOptions), true, true, false); } } private static object Execute(ConsoleEventArgs args) { //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Expected O, but got Unknown if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return "This command can only run on the server."; } if (args.Length < 2 || string.IsNullOrWhiteSpace(args.ArgsAll)) { return "Usage: " + DiscordToolsPlugin.CommandName.Value + " {playerNameOrSteamID}"; } ClientLogRpc.Register(); string text = args.ArgsAll.Trim(); List<ZNetPeer> list = PlayerResolver.FindPeers(text); if (list.Count == 0) { return "No connected player matched '" + text + "'."; } if (list.Count > 1) { return "Multiple connected players matched '" + text + "': " + string.Join(", ", list.Select(PlayerResolver.DescribePeer).ToArray()); } ZNetPeer val = list[0]; string text2 = Guid.NewGuid().ToString("N"); ZPackage val2 = new ZPackage(); val2.Write(text2); val2.Write("manual"); val2.Write(DiscordToolsPlugin.ManualRequestTimeoutSeconds.Value); ZRoutedRpc.instance.InvokeRoutedRPC(val.m_uid, "DiscordTools_RequestLog", new object[1] { val2 }); Terminal context = args.Context; if (context != null) { context.AddString("Requested client log from " + PlayerResolver.DescribePeer(val) + "."); } return true; } private static List<string> GetPlayerOptions() { if ((Object)(object)ZNet.instance == (Object)null) { return new List<string>(); } return (from name in (from peer in ZNet.instance.GetConnectedPeers() where peer.IsReady() select peer.m_playerName into name where !string.IsNullOrWhiteSpace(name) select name).Distinct() orderby name select name).ToList(); } } internal static class ClientLogRpc { private static ZRoutedRpc? _registeredRpc; public static void Register() { if (ZRoutedRpc.instance != null && _registeredRpc != ZRoutedRpc.instance) { _registeredRpc = ZRoutedRpc.instance; ZRoutedRpc.instance.Register<ZPackage>("DiscordTools_RequestLog", (Action<long, ZPackage>)OnRequestLog); ZRoutedRpc.instance.Register<ZPackage>("DiscordTools_LogMeta", (Action<long, ZPackage>)ServerLogReceiver.OnMetadata); ZRoutedRpc.instance.Register<ZPackage>("DiscordTools_LogChunk", (Action<long, ZPackage>)ServerLogReceiver.OnChunk); ZRoutedRpc.instance.Register<ZPackage>("DiscordTools_LogResult", (Action<long, ZPackage>)ClientLogUploader.OnResult); DiscordToolsPlugin.Log.LogInfo((object)"Registered DiscordTools RPC handlers."); } } private static void OnRequestLog(long sender, ZPackage pkg) { if (!((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer()) { string requestId = pkg.ReadString(); string reason = pkg.ReadString(); int timeoutSeconds = pkg.ReadInt(); ClientLogUploader.StartUpload(reason, requestId, timeoutSeconds, null); } } } internal static class ClientLogUploader { private sealed class PreparedLog { public string RequestId = ""; public string Reason = ""; public string LogPath = ""; public long OriginalBytes; public int CompressedBytes; public byte[] CompressedBytesArray = Array.Empty<byte>(); public string Sha256 = ""; public int ChunkSize; public int ChunkCount; public string ClientPlayerName = ""; public string LogModifiedUtc = ""; } private sealed class UploadResult { public bool Success; public string Message = ""; } private static readonly Dictionary<string, UploadResult> Results = new Dictionary<string, UploadResult>(); private static readonly HashSet<string> ActiveReasons = new HashSet<string>(); public static void StartUpload(string reason, string requestId, int timeoutSeconds, Action? continueAfter) { DiscordToolsPlugin instance = DiscordToolsPlugin.Instance; if ((Object)(object)instance == (Object)null) { continueAfter?.Invoke(); return; } if (!ShouldUpload()) { continueAfter?.Invoke(); return; } if (ActiveReasons.Contains(reason)) { continueAfter?.Invoke(); return; } ActiveReasons.Add(reason); ((MonoBehaviour)instance).StartCoroutine(UploadRoutine(reason, requestId, timeoutSeconds, continueAfter)); } public static void OnResult(long sender, ZPackage pkg) { string key = pkg.ReadString(); Results[key] = new UploadResult { Success = pkg.ReadBool(), Message = pkg.ReadString() }; } public static bool ShouldUpload() { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Invalid comparison between Unknown and I4 if ((Object)(object)ZNet.instance != (Object)null && ZRoutedRpc.instance != null && !ZNet.instance.IsServer()) { return (int)ZNet.GetConnectionStatus() == 2; } return false; } private static IEnumerator UploadRoutine(string reason, string requestId, int timeoutSeconds, Action? continueAfter) { string reason2 = reason; string requestId2 = requestId; PreparedLog prepared = null; Exception error = null; Task<PreparedLog> prepareTask = Task.Run(() => PrepareLog(reason2, requestId2)); while (!prepareTask.IsCompleted) { yield return null; } if (prepareTask.IsFaulted) { error = prepareTask.Exception?.GetBaseException(); } else { prepared = prepareTask.Result; } if (error != null || prepared == null) { DiscordToolsPlugin.Log.LogWarning((object)("Could not prepare client log: " + (error?.Message ?? "unknown error"))); ActiveReasons.Remove(reason2); continueAfter?.Invoke(); yield break; } if (prepared.OriginalBytes > DiscordToolsPlugin.MaxOriginalBytes.Value) { DiscordToolsPlugin.Log.LogWarning((object)"Client log is larger than MaxOriginalBytes. Upload skipped."); ActiveReasons.Remove(reason2); continueAfter?.Invoke(); yield break; } if (prepared.CompressedBytes > DiscordToolsPlugin.MaxCompressedBytes.Value) { DiscordToolsPlugin.Log.LogWarning((object)"Compressed client log is larger than MaxCompressedBytes. Upload skipped."); ActiveReasons.Remove(reason2); continueAfter?.Invoke(); yield break; } SendMetadata(prepared); for (int i = 0; i < prepared.ChunkCount; i++) { int num = i * prepared.ChunkSize; int num2 = Math.Min(prepared.ChunkSize, prepared.CompressedBytes - num); byte[] array = new byte[num2]; Buffer.BlockCopy(prepared.CompressedBytesArray, num, array, 0, num2); ZPackage val = new ZPackage(); val.Write(prepared.RequestId); val.Write(i); val.Write(array); ZRoutedRpc.instance.InvokeRoutedRPC("DiscordTools_LogChunk", new object[1] { val }); if (i % 8 == 7) { yield return null; } } float deadline = Time.realtimeSinceStartup + (float)Math.Max(1, timeoutSeconds); while (!Results.ContainsKey(prepared.RequestId) && Time.realtimeSinceStartup < deadline) { yield return null; } if (Results.TryGetValue(prepared.RequestId, out UploadResult value)) { DiscordToolsPlugin.Log.LogInfo((object)("Client log upload result: " + value.Message)); Results.Remove(prepared.RequestId); } else { DiscordToolsPlugin.Log.LogWarning((object)"Client log upload timed out waiting for server acknowledgement."); } ActiveReasons.Remove(reason2); continueAfter?.Invoke(); } private static PreparedLog PrepareLog(string reason, string requestId) { string text = Path.Combine(Paths.BepInExRootPath, "LogOutput.log"); if (!File.Exists(text)) { string fullPath = Path.GetFullPath(Path.Combine(Paths.BepInExRootPath, "..", "LogOutput.log")); if (File.Exists(fullPath)) { text = fullPath; } } if (!File.Exists(text)) { throw new FileNotFoundException("Could not find LogOutput.log.", text); } byte[] array; using (FileStream fileStream = new FileStream(text, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { using MemoryStream memoryStream = new MemoryStream(); fileStream.CopyTo(memoryStream); array = memoryStream.ToArray(); } byte[] array2; using (MemoryStream memoryStream2 = new MemoryStream()) { using (GZipStream gZipStream = new GZipStream(memoryStream2, CompressionLevel.Optimal, leaveOpen: true)) { gZipStream.Write(array, 0, array.Length); } array2 = memoryStream2.ToArray(); } int num = Mathf.Clamp(DiscordToolsPlugin.ChunkSizeBytes.Value, 4096, 262144); FileInfo fileInfo = new FileInfo(text); return new PreparedLog { RequestId = requestId, Reason = reason, LogPath = text, OriginalBytes = array.Length, CompressedBytes = array2.Length, CompressedBytesArray = array2, Sha256 = Sha256Hex(array2), ChunkSize = num, ChunkCount = (int)Math.Ceiling((double)array2.Length / (double)num), ClientPlayerName = GetLocalPlayerName(), LogModifiedUtc = fileInfo.LastWriteTimeUtc.ToString("O", CultureInfo.InvariantCulture) }; } private static void SendMetadata(PreparedLog prepared) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(prepared.RequestId); val.Write(prepared.Reason); val.Write(prepared.OriginalBytes); val.Write((long)prepared.CompressedBytes); val.Write(prepared.Sha256); val.Write(prepared.ChunkSize); val.Write(prepared.ChunkCount); val.Write(prepared.ClientPlayerName); val.Write(prepared.LogModifiedUtc); ZRoutedRpc.instance.InvokeRoutedRPC("DiscordTools_LogMeta", new object[1] { val }); } private static string GetLocalPlayerName() { try { return ((Object)(object)Game.instance != (Object)null) ? Game.instance.GetPlayerProfile().GetName() : ""; } catch { return ""; } } private static string Sha256Hex(byte[] bytes) { using SHA256 sHA = SHA256.Create(); return BitConverter.ToString(sHA.ComputeHash(bytes)).Replace("-", "").ToLowerInvariant(); } } internal static class LogArchive { private sealed class MetadataEntry { public string PlayerId = ""; public string PlayerName = ""; public string PlayerFolder = ""; public string Reason = ""; public string Path = ""; public DateTime ReceivedAtUtc; } public static string Root => ResolveRoot(); private static string PlayersDir => Path.Combine(Root, "players"); private static string IndexDir => Path.Combine(Root, "index"); private static string IncomingDir => Path.Combine(Root, "incoming"); private static string BotUploadFailedDir => Path.Combine(Root, "bot-upload-failed"); public static void EnsureDirectories() { Directory.CreateDirectory(PlayersDir); Directory.CreateDirectory(IndexDir); Directory.CreateDirectory(IncomingDir); Directory.CreateDirectory(BotUploadFailedDir); } public static string GetIncomingPath(string requestId) { EnsureDirectories(); return Path.Combine(IncomingDir, SafePathSegment(requestId) + ".tmp"); } public static ArchivedLog Archive(IncomingTransfer transfer) { EnsureDirectories(); string playerId = SafePathSegment(PlayerResolver.StablePlayerId(transfer.Peer)); string text = (string.IsNullOrWhiteSpace(transfer.Peer.m_playerName) ? transfer.ClientPlayerName : transfer.Peer.m_playerName); text = (string.IsNullOrWhiteSpace(text) ? "unknown" : text); string text2 = BuildPlayerFolderName(text, playerId); string playerFolder = "players/" + text2; string path = transfer.ReceivedAtUtc.ToString("yyyy-MM", CultureInfo.InvariantCulture); string text3 = transfer.ReceivedAtUtc.ToString("yyyy-MM-dd_HH-mm-ss'Z'", CultureInfo.InvariantCulture); string text4 = text3 + "_" + transfer.Reason + "_" + SafePathSegment(text) + "_LogOutput"; string text5 = Path.Combine(PlayersDir, text2); string text6 = Path.Combine(text5, "logs", path); Directory.CreateDirectory(text6); string text7 = Path.Combine(text6, text4 + ".log.gz"); string text8 = Path.Combine(text6, text4 + ".json"); File.Copy(transfer.TempPath, text7, overwrite: true); ArchivedLog archivedLog = new ArchivedLog { PlayerId = playerId, PlayerName = text, PlayerFolder = playerFolder, Reason = transfer.Reason, RequestId = transfer.RequestId, ReceivedAtUtc = transfer.ReceivedAtUtc, OriginalBytes = transfer.OriginalBytes, CompressedBytes = transfer.CompressedBytes, Sha256 = transfer.Sha256, LogPath = text7, MetadataPath = text8, RelativeLogPath = ToRelative(text7), RelativeMetadataPath = ToRelative(text8) }; File.WriteAllText(text8, BuildLogMetadataJson(archivedLog, transfer), Encoding.UTF8); WritePlayerFiles(text5, archivedLog); RebuildIndexes(); return archivedLog; } public static void CleanupOldLogs() { int value = DiscordToolsPlugin.RetentionDays.Value; if (value <= 0 || !Directory.Exists(PlayersDir)) { return; } DateTime dateTime = DateTime.UtcNow.AddDays(-value); string[] files = Directory.GetFiles(PlayersDir, "*", SearchOption.AllDirectories); foreach (string path in files) { if (IsArchivedLogFile(path) && File.GetLastWriteTimeUtc(path) < dateTime) { TryDelete(path); } } RebuildIndexes(); } public static void MarkBotUploadFailed(ArchivedLog log, string message) { Directory.CreateDirectory(BotUploadFailedDir); File.WriteAllText(Path.Combine(BotUploadFailedDir, Path.GetFileName(log.MetadataPath)), JsonObject(new Dictionary<string, string> { ["playerId"] = log.PlayerId, ["playerName"] = log.PlayerName, ["playerFolder"] = log.PlayerFolder, ["reason"] = log.Reason, ["receivedAtUtc"] = log.ReceivedAtUtc.ToString("O", CultureInfo.InvariantCulture), ["path"] = log.RelativeLogPath, ["error"] = message }), Encoding.UTF8); } private static void WritePlayerFiles(string playerDir, ArchivedLog latest) { string path = Path.Combine(playerDir, "player.json"); SortedSet<string> sortedSet = new SortedSet<string>(StringComparer.OrdinalIgnoreCase) { latest.PlayerName }; if (File.Exists(path)) { foreach (Match item in Regex.Matches(File.ReadAllText(path), "\"knownNames\"\\s*:\\s*\\[(.*?)\\]", RegexOptions.Singleline)) { foreach (Match item2 in Regex.Matches(item.Groups[1].Value, "\"(.*?)\"")) { sortedSet.Add(UnescapeJson(item2.Groups[1].Value)); } } } File.WriteAllText(path, BuildPlayerJson(latest, sortedSet), Encoding.UTF8); File.WriteAllText(Path.Combine(playerDir, "latest.json"), JsonObject(new Dictionary<string, string> { ["latestLog"] = latest.RelativeLogPath, ["latestMetadata"] = latest.RelativeMetadataPath, ["receivedAtUtc"] = latest.ReceivedAtUtc.ToString("O", CultureInfo.InvariantCulture), ["reason"] = latest.Reason, ["playerId"] = latest.PlayerId, ["playerFolder"] = latest.PlayerFolder, ["playerName"] = latest.PlayerName }), Encoding.UTF8); } private static void RebuildIndexes() { EnsureDirectories(); List<MetadataEntry> list = (from entry in ReadAllMetadata() orderby entry.ReceivedAtUtc descending select entry).ToList(); SortedDictionary<string, string> sortedDictionary = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase); SortedDictionary<string, SortedSet<string>> sortedDictionary2 = new SortedDictionary<string, SortedSet<string>>(StringComparer.OrdinalIgnoreCase); SortedDictionary<string, SortedSet<string>> sortedDictionary3 = new SortedDictionary<string, SortedSet<string>>(StringComparer.OrdinalIgnoreCase); SortedDictionary<string, SortedSet<string>> sortedDictionary4 = new SortedDictionary<string, SortedSet<string>>(StringComparer.OrdinalIgnoreCase); foreach (MetadataEntry item in list) { string value = LegacyPlayerFolder(item.PlayerId); if (!sortedDictionary.TryGetValue(item.PlayerId, out var _) || item.PlayerFolder.Equals(value, StringComparison.OrdinalIgnoreCase)) { sortedDictionary[item.PlayerId] = item.PlayerFolder; } AddToStringSetMap(sortedDictionary3, item.PlayerId, item.PlayerFolder); string text = item.PlayerName.Trim().ToLowerInvariant(); if (text.Length != 0) { AddToStringSetMap(sortedDictionary2, text, item.PlayerId); AddToStringSetMap(sortedDictionary4, text, item.PlayerFolder); } } File.WriteAllText(Path.Combine(IndexDir, "players.json"), BuildPlayersIndexJson(sortedDictionary, sortedDictionary2, sortedDictionary3, sortedDictionary4), Encoding.UTF8); File.WriteAllText(Path.Combine(IndexDir, "recent.json"), BuildRecentJson(list.Take(100).ToList()), Encoding.UTF8); } private static List<MetadataEntry> ReadAllMetadata() { List<MetadataEntry> list = new List<MetadataEntry>(); if (!Directory.Exists(PlayersDir)) { return list; } string[] files = Directory.GetFiles(PlayersDir, "*.json", SearchOption.AllDirectories); foreach (string text in files) { if (Path.GetFileName(text).Equals("player.json", StringComparison.OrdinalIgnoreCase) || Path.GetFileName(text).Equals("latest.json", StringComparison.OrdinalIgnoreCase)) { continue; } try { string json = File.ReadAllText(text); MetadataEntry metadataEntry = new MetadataEntry { PlayerId = JsonValue(json, "playerId"), PlayerName = JsonValue(json, "playerName"), Reason = JsonValue(json, "reason"), Path = JsonValue(json, "logPath") }; metadataEntry.PlayerFolder = JsonValue(json, "playerFolder"); if (string.IsNullOrWhiteSpace(metadataEntry.PlayerFolder)) { metadataEntry.PlayerFolder = PlayerFolderFromRelativeLogPath(metadataEntry.Path, metadataEntry.PlayerId); } if (!DateTime.TryParse(JsonValue(json, "receivedAtUtc"), null, DateTimeStyles.RoundtripKind, out metadataEntry.ReceivedAtUtc)) { metadataEntry.ReceivedAtUtc = File.GetLastWriteTimeUtc(text); } if (!string.IsNullOrWhiteSpace(metadataEntry.PlayerId)) { list.Add(metadataEntry); } } catch (Exception ex) { DiscordToolsPlugin.Log.LogWarning((object)("Could not read metadata " + text + ": " + ex.Message)); } } return list; } private static string ResolveRoot() { string text = DiscordToolsPlugin.OutputDirectory.Value; if (string.IsNullOrWhiteSpace(text)) { text = "client-logs"; } if (!Path.IsPathRooted(text)) { return Path.Combine(Paths.BepInExRootPath, text); } return text; } private static bool IsArchivedLogFile(string path) { string text = path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); char directorySeparatorChar = Path.DirectorySeparatorChar; string text2 = directorySeparatorChar.ToString(); directorySeparatorChar = Path.DirectorySeparatorChar; string value = text2 + "logs" + directorySeparatorChar; if (text.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) { if (!path.EndsWith(".log.gz", StringComparison.OrdinalIgnoreCase)) { return path.EndsWith(".json", StringComparison.OrdinalIgnoreCase); } return true; } return false; } private static string BuildLogMetadataJson(ArchivedLog archived, IncomingTransfer transfer) { return JsonObject(new Dictionary<string, string> { ["requestId"] = archived.RequestId, ["reason"] = archived.Reason, ["playerId"] = archived.PlayerId, ["playerName"] = archived.PlayerName, ["playerFolder"] = archived.PlayerFolder, ["clientPlayerName"] = transfer.ClientPlayerName, ["receivedAtUtc"] = archived.ReceivedAtUtc.ToString("O", CultureInfo.InvariantCulture), ["logModifiedUtc"] = transfer.LogModifiedUtc, ["originalBytes"] = archived.OriginalBytes.ToString(CultureInfo.InvariantCulture), ["compressedBytes"] = archived.CompressedBytes.ToString(CultureInfo.InvariantCulture), ["compression"] = "gzip", ["sha256"] = archived.Sha256, ["logPath"] = archived.RelativeLogPath, ["metadataPath"] = archived.RelativeMetadataPath }); } private static string BuildPlayerJson(ArchivedLog latest, IEnumerable<string> knownNames) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("{\n"); AppendJsonProperty(stringBuilder, "playerId", latest.PlayerId, comma: true); AppendJsonProperty(stringBuilder, "lastKnownName", latest.PlayerName, comma: true); AppendJsonProperty(stringBuilder, "playerFolder", latest.PlayerFolder, comma: true); stringBuilder.Append(" \"knownNames\": ["); stringBuilder.Append(string.Join(", ", knownNames.Select((string name) => "\"" + EscapeJson(name) + "\"").ToArray())); stringBuilder.Append("],\n"); AppendJsonProperty(stringBuilder, "lastSeenUtc", latest.ReceivedAtUtc.ToString("O", CultureInfo.InvariantCulture), comma: false); stringBuilder.Append("}\n"); return stringBuilder.ToString(); } private static string BuildPlayersIndexJson(SortedDictionary<string, string> byId, SortedDictionary<string, SortedSet<string>> byName, SortedDictionary<string, SortedSet<string>> byIdFolders, SortedDictionary<string, SortedSet<string>> byNameFolders) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("{\n \"byId\": {\n"); AppendStringMap(stringBuilder, byId, 4); stringBuilder.Append("\n },\n \"byName\": {\n"); AppendStringSetMap(stringBuilder, byName, 4); stringBuilder.Append(" },\n \"byIdFolders\": {\n"); AppendStringSetMap(stringBuilder, byIdFolders, 4); stringBuilder.Append(" },\n \"byNameFolders\": {\n"); AppendStringSetMap(stringBuilder, byNameFolders, 4); stringBuilder.Append(" }\n}\n"); return stringBuilder.ToString(); } private static string BuildRecentJson(List<MetadataEntry> entries) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("[\n"); for (int i = 0; i < entries.Count; i++) { MetadataEntry metadataEntry = entries[i]; stringBuilder.Append(" {\n"); AppendJsonProperty(stringBuilder, "playerId", metadataEntry.PlayerId, comma: true, 4); AppendJsonProperty(stringBuilder, "playerName", metadataEntry.PlayerName, comma: true, 4); AppendJsonProperty(stringBuilder, "playerFolder", metadataEntry.PlayerFolder, comma: true, 4); AppendJsonProperty(stringBuilder, "reason", metadataEntry.Reason, comma: true, 4); AppendJsonProperty(stringBuilder, "receivedAtUtc", metadataEntry.ReceivedAtUtc.ToString("O", CultureInfo.InvariantCulture), comma: true, 4); AppendJsonProperty(stringBuilder, "path", metadataEntry.Path, comma: false, 4); stringBuilder.Append(" }"); if (i + 1 < entries.Count) { stringBuilder.Append(","); } stringBuilder.Append("\n"); } stringBuilder.Append("]\n"); return stringBuilder.ToString(); } private static string JsonObject(Dictionary<string, string> values) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("{\n"); int num = 0; foreach (KeyValuePair<string, string> value in values) { AppendJsonProperty(stringBuilder, value.Key, value.Value, ++num < values.Count); } stringBuilder.Append("}\n"); return stringBuilder.ToString(); } private static void AddToStringSetMap(SortedDictionary<string, SortedSet<string>> map, string key, string value) { if (!map.TryGetValue(key, out SortedSet<string> value2)) { value2 = (map[key] = new SortedSet<string>(StringComparer.OrdinalIgnoreCase)); } value2.Add(value); } private static void AppendStringMap(StringBuilder builder, SortedDictionary<string, string> map, int indent) { string value = new string(' ', indent); int num = 0; foreach (KeyValuePair<string, string> item in map) { builder.Append(value).Append("\"").Append(EscapeJson(item.Key)) .Append("\": \"") .Append(EscapeJson(item.Value)) .Append("\""); if (++num < map.Count) { builder.Append(","); } builder.Append("\n"); } } private static void AppendStringSetMap(StringBuilder builder, SortedDictionary<string, SortedSet<string>> map, int indent) { string value2 = new string(' ', indent); int num = 0; foreach (KeyValuePair<string, SortedSet<string>> item in map) { builder.Append(value2).Append("\"").Append(EscapeJson(item.Key)) .Append("\": ["); builder.Append(string.Join(", ", item.Value.Select((string value) => "\"" + EscapeJson(value) + "\"").ToArray())); builder.Append("]"); if (++num < map.Count) { builder.Append(","); } builder.Append("\n"); } } private static void AppendJsonProperty(StringBuilder builder, string key, string value, bool comma, int indent = 2) { builder.Append(new string(' ', indent)).Append("\"").Append(EscapeJson(key)) .Append("\": ") .Append("\"") .Append(EscapeJson(value)) .Append("\""); if (comma) { builder.Append(","); } builder.Append("\n"); } private static string JsonValue(string json, string key) { Match match = Regex.Match(json, "\"" + Regex.Escape(key) + "\"\\s*:\\s*\"((?:\\\\.|[^\"])*)\""); if (!match.Success) { return ""; } return UnescapeJson(match.Groups[1].Value); } private static string PlayerFolderFromRelativeLogPath(string relativeLogPath, string playerId) { string text = (relativeLogPath ?? "").Replace('\\', '/'); int num = text.IndexOf("/logs/", StringComparison.OrdinalIgnoreCase); if (text.StartsWith("players/", StringComparison.OrdinalIgnoreCase) && num > "players/".Length) { return text.Substring(0, num); } return LegacyPlayerFolder(playerId); } private static string EscapeJson(string value) { return (value ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "\\r") .Replace("\n", "\\n"); } private static string UnescapeJson(string value) { return (value ?? "").Replace("\\n", "\n").Replace("\\r", "\r").Replace("\\\"", "\"") .Replace("\\\\", "\\"); } private static string SafePathSegment(string value) { value = (string.IsNullOrWhiteSpace(value) ? "unknown" : value.Trim()); char[] invalid = Path.GetInvalidFileNameChars(); string input = new string(value.Select((char ch) => (!invalid.Contains(ch) && !char.IsWhiteSpace(ch)) ? ch : '_').ToArray()); input = Regex.Replace(input, "_+", "_").Trim(new char[1] { '_' }); if (input.Length != 0) { return input; } return "unknown"; } private static string BuildPlayerFolderName(string playerName, string playerId) { return SafePathSegment(playerName) + "_" + SafePathSegment(playerId); } private static string LegacyPlayerFolder(string playerId) { return "players/" + SafePathSegment(playerId); } private static string ToRelative(string path) { string text = Root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); char directorySeparatorChar = Path.DirectorySeparatorChar; string text2 = text + directorySeparatorChar; if (!path.StartsWith(text2, StringComparison.OrdinalIgnoreCase)) { return path; } return path.Substring(text2.Length).Replace(Path.DirectorySeparatorChar, '/'); } private static void TryDelete(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch { } } } internal sealed class IncomingTransfer { public long Sender; public ZNetPeer Peer; public string RequestId = ""; public string Reason = ""; public long OriginalBytes; public long CompressedBytes; public string Sha256 = ""; public int ChunkSize; public int ChunkCount; public string ClientPlayerName = ""; public string LogModifiedUtc = ""; public DateTime ReceivedAtUtc; public string TempPath = ""; public bool[] ReceivedChunks = Array.Empty<bool>(); public int ReceivedCount; } internal sealed class ArchivedLog { public string PlayerId = ""; public string PlayerName = ""; public string PlayerFolder = ""; public string Reason = ""; public string RequestId = ""; public DateTime ReceivedAtUtc; public long OriginalBytes; public long CompressedBytes; public string Sha256 = ""; public string LogPath = ""; public string MetadataPath = ""; public string RelativeLogPath = ""; public string RelativeMetadataPath = ""; } [HarmonyPatch(typeof(ZNet), "Awake")] internal static class ZNetAwakePatch { private static void Postfix() { ClientLogRpc.Register(); } } [HarmonyPatch(typeof(Game), "Logout")] internal static class GameLogoutPatch { private static bool _continuing; private static bool Prefix(Game __instance, bool save, bool changeToStartScene) { Game __instance2 = __instance; if (_continuing || !ClientLogUploader.ShouldUpload()) { return true; } ClientLogUploader.StartUpload("logout", Guid.NewGuid().ToString("N"), DiscordToolsPlugin.LogoutUploadTimeoutSeconds.Value, delegate { _continuing = true; try { __instance2.Logout(save, changeToStartScene); } finally { _continuing = false; } }); return false; } } [HarmonyPatch(typeof(Menu), "QuitGame")] internal static class MenuQuitPatch { private static bool _continuing; private static bool Prefix() { if (_continuing || !ClientLogUploader.ShouldUpload()) { return true; } ClientLogUploader.StartUpload("quit", Guid.NewGuid().ToString("N"), DiscordToolsPlugin.QuitUploadTimeoutSeconds.Value, delegate { _continuing = true; try { Gogan.LogEvent("Game", "Quit", "", 0L); Application.Quit(); } finally { _continuing = false; } }); return false; } } internal static class PlayerResolver { public static List<ZNetPeer> FindPeers(string query) { List<ZNetPeer> list = new List<ZNetPeer>(); if ((Object)(object)ZNet.instance == (Object)null) { return list; } string text = Normalize(query); foreach (ZNetPeer connectedPeer in ZNet.instance.GetConnectedPeers()) { if (connectedPeer.IsReady()) { string value = SafeHostName(connectedPeer); string value2 = StablePlayerId(connectedPeer); if (Normalize(connectedPeer.m_playerName) == text || Normalize(value) == text || Normalize(value2) == text || (DigitsOnly(value) == DigitsOnly(query) && DigitsOnly(query).Length > 0)) { list.Add(connectedPeer); } } } if (list.Count > 0) { return list; } foreach (ZNetPeer connectedPeer2 in ZNet.instance.GetConnectedPeers()) { if (connectedPeer2.IsReady() && Normalize(connectedPeer2.m_playerName).Contains(text)) { list.Add(connectedPeer2); } } return list; } public static ZNetPeer? FindPeerBySender(long sender) { if ((Object)(object)ZNet.instance == (Object)null) { return null; } return ((IEnumerable<ZNetPeer>)ZNet.instance.GetConnectedPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer peer) => peer.m_uid == sender)); } public static string DescribePeer(ZNetPeer peer) { return peer.m_playerName + " (" + StablePlayerId(peer) + ")"; } public static string StablePlayerId(ZNetPeer peer) { string text = SafeHostName(peer); if (!string.IsNullOrWhiteSpace(text)) { return text; } return peer.m_uid.ToString(CultureInfo.InvariantCulture); } public static string SafeHostName(ZNetPeer peer) { try { ISocket socket = peer.m_socket; return ((socket != null) ? socket.GetHostName() : null) ?? ""; } catch { return ""; } } public static string SafeEndPoint(ZNetPeer peer) { try { ISocket socket = peer.m_socket; return ((socket != null) ? socket.GetEndPointString() : null) ?? ""; } catch { return ""; } } public static string PlatformDisplayName(ZNetPeer peer) { try { string value; return (peer.m_serverSyncedPlayerData != null && peer.m_serverSyncedPlayerData.TryGetValue("platformDisplayName", out value)) ? value : ""; } catch { return ""; } } private static string Normalize(string value) { return (value ?? "").Trim().ToLowerInvariant(); } private static string DigitsOnly(string value) { return new string((value ?? "").Where(char.IsDigit).ToArray()); } } [BepInPlugin("warpalicious.DiscordTools", "DiscordTools", "1.4.0")] public class DiscordToolsPlugin : BaseUnityPlugin { private const string ModName = "DiscordTools"; private const string ModVersion = "1.4.0"; private const string Author = "warpalicious"; private const string ModGUID = "warpalicious.DiscordTools"; private const string BotApiUrlEnv = "DISCORDTOOLS_BOT_API_URL"; private const string BotApiKeyEnv = "DISCORDTOOLS_BOT_API_KEY"; private readonly Harmony _harmony = new Harmony("warpalicious.DiscordTools"); private DateTime _lastReloadTime; private const long ReloadDelayTicks = 10000000L; public static readonly ManualLogSource Log = Logger.CreateLogSource("DiscordTools"); internal static ConfigEntry<string> CommandName = null; internal static ConfigEntry<string> OutputDirectory = null; internal static ConfigEntry<int> ChunkSizeBytes = null; internal static ConfigEntry<int> ManualRequestTimeoutSeconds = null; internal static ConfigEntry<int> LogoutUploadTimeoutSeconds = null; internal static ConfigEntry<int> QuitUploadTimeoutSeconds = null; internal static ConfigEntry<int> RetentionDays = null; internal static ConfigEntry<bool> DeleteOldLogsOnStartup = null; internal static ConfigEntry<long> MaxOriginalBytes = null; internal static ConfigEntry<long> MaxCompressedBytes = null; internal static ConfigEntry<bool> PostToBotApi = null; internal static ConfigEntry<string> BotApiUrl = null; internal static ConfigEntry<string> BotApiKey = null; public static DiscordToolsPlugin? Instance { get; private set; } internal static string GetBotApiUrl() { string environmentVariable = Environment.GetEnvironmentVariable("DISCORDTOOLS_BOT_API_URL"); if (!string.IsNullOrWhiteSpace(environmentVariable)) { return environmentVariable.Trim(); } return BotApiUrl.Value; } internal static string GetBotApiKey() { string environmentVariable = Environment.GetEnvironmentVariable("DISCORDTOOLS_BOT_API_KEY"); if (!string.IsNullOrWhiteSpace(environmentVariable)) { return environmentVariable.Trim(); } return BotApiKey.Value; } public void Awake() { Instance = this; BindConfig(); ClientLogCommand.Register(); _harmony.PatchAll(Assembly.GetExecutingAssembly()); SetupWatcher(); LogArchive.EnsureDirectories(); if (DeleteOldLogsOnStartup.Value) { LogArchive.CleanupOldLogs(); } } private void OnDestroy() { ((BaseUnityPlugin)this).Config.Save(); _harmony.UnpatchSelf(); if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } private void BindConfig() { CommandName = ((BaseUnityPlugin)this).Config.Bind<string>("General", "CommandName", "client-logs", "Server command used by RCON to request a connected client's log."); OutputDirectory = ((BaseUnityPlugin)this).Config.Bind<string>("General", "OutputDirectory", "client-logs", "Log archive directory. Relative paths are placed under BepInEx."); ChunkSizeBytes = ((BaseUnityPlugin)this).Config.Bind<int>("General", "ChunkSizeBytes", 32768, "Compressed upload chunk size sent through Valheim networking."); ManualRequestTimeoutSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("General", "ManualRequestTimeoutSeconds", 120, "How long the client waits for the server to acknowledge a manual log request."); LogoutUploadTimeoutSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("General", "LogoutUploadTimeoutSeconds", 30, "How long logout waits for log upload before continuing."); QuitUploadTimeoutSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("General", "QuitUploadTimeoutSeconds", 10, "How long normal quit waits for log upload before continuing."); RetentionDays = ((BaseUnityPlugin)this).Config.Bind<int>("General", "RetentionDays", 30, "Delete archived logs older than this many days. Set 0 to keep logs forever."); DeleteOldLogsOnStartup = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "DeleteOldLogsOnStartup", true, "Run retention cleanup when the mod loads."); MaxOriginalBytes = ((BaseUnityPlugin)this).Config.Bind<long>("Limits", "MaxOriginalBytes", 104857600L, "Largest uncompressed client log accepted, in bytes."); MaxCompressedBytes = ((BaseUnityPlugin)this).Config.Bind<long>("Limits", "MaxCompressedBytes", 52428800L, "Largest compressed client log accepted, in bytes."); PostToBotApi = ((BaseUnityPlugin)this).Config.Bind<bool>("BotApi", "PostToBotApi", true, "Upload received logs to a compatible Discord bot API."); BotApiUrl = ((BaseUnityPlugin)this).Config.Bind<string>("BotApi", "ApiUrl", "", "Compatible bot client-log upload endpoint. Prefer the DISCORDTOOLS_BOT_API_URL environment variable on dedicated servers."); BotApiKey = ((BaseUnityPlugin)this).Config.Bind<string>("BotApi", "ApiKey", "", "API key sent to the bot in the X-API-Key header. Prefer the DISCORDTOOLS_BOT_API_KEY environment variable on dedicated servers."); } private void SetupWatcher() { _lastReloadTime = DateTime.Now; FileSystemWatcher fileSystemWatcher = new FileSystemWatcher(Paths.ConfigPath, "warpalicious.DiscordTools.cfg"); fileSystemWatcher.Changed += ReadConfigValues; fileSystemWatcher.Created += ReadConfigValues; fileSystemWatcher.Renamed += ReadConfigValues; fileSystemWatcher.IncludeSubdirectories = true; fileSystemWatcher.EnableRaisingEvents = true; } private void ReadConfigValues(object sender, FileSystemEventArgs e) { DateTime now = DateTime.Now; long num = now.Ticks - _lastReloadTime.Ticks; if (File.Exists(Path.Combine(Paths.ConfigPath, "warpalicious.DiscordTools.cfg")) && num >= 10000000) { try { Log.LogInfo((object)"Reloading configuration."); ((BaseUnityPlugin)this).Config.Reload(); LogArchive.EnsureDirectories(); } catch (Exception ex) { Log.LogError((object)("Failed to reload configuration: " + ex.Message)); } _lastReloadTime = now; } } } internal static class RpcNames { public const string RequestLog = "DiscordTools_RequestLog"; public const string LogMeta = "DiscordTools_LogMeta"; public const string LogChunk = "DiscordTools_LogChunk"; public const string LogResult = "DiscordTools_LogResult"; } internal static class ServerLogReceiver { private static readonly Dictionary<string, IncomingTransfer> Transfers = new Dictionary<string, IncomingTransfer>(); public static void OnMetadata(long sender, ZPackage pkg) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } ZNetPeer val = PlayerResolver.FindPeerBySender(sender); if (val != null) { IncomingTransfer incomingTransfer = new IncomingTransfer { Sender = sender, Peer = val, RequestId = pkg.ReadString(), Reason = SafeReason(pkg.ReadString()), OriginalBytes = pkg.ReadLong(), CompressedBytes = pkg.ReadLong(), Sha256 = pkg.ReadString(), ChunkSize = pkg.ReadInt(), ChunkCount = pkg.ReadInt(), ClientPlayerName = pkg.ReadString(), LogModifiedUtc = pkg.ReadString(), ReceivedAtUtc = DateTime.UtcNow }; if (incomingTransfer.OriginalBytes > DiscordToolsPlugin.MaxOriginalBytes.Value) { SendResult(sender, incomingTransfer.RequestId, success: false, "Original log exceeds server limit."); return; } if (incomingTransfer.CompressedBytes > DiscordToolsPlugin.MaxCompressedBytes.Value) { SendResult(sender, incomingTransfer.RequestId, success: false, "Compressed log exceeds server limit."); return; } if (incomingTransfer.ChunkSize <= 0 || incomingTransfer.ChunkCount <= 0 || incomingTransfer.ChunkCount > 100000) { SendResult(sender, incomingTransfer.RequestId, success: false, "Invalid upload metadata."); return; } incomingTransfer.TempPath = LogArchive.GetIncomingPath(incomingTransfer.RequestId); incomingTransfer.ReceivedChunks = new bool[incomingTransfer.ChunkCount]; Transfers[incomingTransfer.RequestId] = incomingTransfer; DiscordToolsPlugin.Log.LogInfo((object)("Receiving client log from " + PlayerResolver.DescribePeer(val) + " reason=" + incomingTransfer.Reason + " request=" + incomingTransfer.RequestId)); } } public static void OnChunk(long sender, ZPackage pkg) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } string key = pkg.ReadString(); if (!Transfers.TryGetValue(key, out IncomingTransfer value) || value.Sender != sender) { return; } int num = pkg.ReadInt(); byte[] array = pkg.ReadByteArray(); if (num >= 0 && num < value.ChunkCount && !value.ReceivedChunks[num]) { Directory.CreateDirectory(Path.GetDirectoryName(value.TempPath)); using (FileStream fileStream = new FileStream(value.TempPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None)) { fileStream.Seek((long)num * (long)value.ChunkSize, SeekOrigin.Begin); fileStream.Write(array, 0, array.Length); } value.ReceivedChunks[num] = true; value.ReceivedCount++; if (value.ReceivedCount >= value.ChunkCount) { FinishTransfer(value); Transfers.Remove(key); } } } private static void FinishTransfer(IncomingTransfer transfer) { try { FileInfo fileInfo = new FileInfo(transfer.TempPath); if (!fileInfo.Exists || fileInfo.Length != transfer.CompressedBytes) { SendResult(transfer.Sender, transfer.RequestId, success: false, "Compressed byte count did not match."); return; } if (!string.Equals(Sha256Hex(File.ReadAllBytes(transfer.TempPath)), transfer.Sha256, StringComparison.OrdinalIgnoreCase)) { SendResult(transfer.Sender, transfer.RequestId, success: false, "Compressed file hash did not match."); return; } ArchivedLog archivedLog = LogArchive.Archive(transfer); SendResult(transfer.Sender, transfer.RequestId, success: true, "Saved client log to " + archivedLog.RelativeLogPath); DiscordToolsPlugin instance = DiscordToolsPlugin.Instance; if ((Object)(object)instance != (Object)null && DiscordToolsPlugin.PostToBotApi.Value) { ((MonoBehaviour)instance).StartCoroutine(BotApiClient.PostLogRoutine(archivedLog)); } } catch (Exception ex) { DiscordToolsPlugin.Log.LogError((object)("Failed to finish client log transfer: " + ex)); SendResult(transfer.Sender, transfer.RequestId, success: false, "Server failed to archive log: " + ex.Message); } finally { TryDelete(transfer.TempPath); } } private static void SendResult(long target, string requestId, bool success, string message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(requestId); val.Write(success); val.Write(message); ZRoutedRpc.instance.InvokeRoutedRPC(target, "DiscordTools_LogResult", new object[1] { val }); } private static string Sha256Hex(byte[] bytes) { using SHA256 sHA = SHA256.Create(); return BitConverter.ToString(sHA.ComputeHash(bytes)).Replace("-", "").ToLowerInvariant(); } private static string SafeReason(string reason) { reason = (reason ?? "").Trim().ToLowerInvariant(); switch (reason) { default: return "unknown"; case "logout": case "quit": case "manual": return reason; } } private static void TryDelete(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch { } } } }