Decompiled source of DiscordTools v1.4.0

DiscordTools.dll

Decompiled a day ago
using 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
			{
			}
		}
	}
}