Decompiled source of BetterModeration v1.0.0

Mods/BetterModeration.dll

Decompiled a day ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BetterModeration;
using HarmonyLib;
using LabFusion.Marrow.Proxies;
using LabFusion.Menu;
using LabFusion.Network;
using LabFusion.Player;
using LabFusion.Senders;
using LabFusion.UI.Popups;
using LabFusion.Utilities;
using MelonLoader;
using MelonLoader.Utils;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: MelonInfo(typeof(Main), "Better Moderation", "1.0.0", "armyk_r3lv", null)]
[assembly: MelonGame("Stress Level Zero", "BONELAB")]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyCompany("BetterModeration")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("BetterModeration")]
[assembly: AssemblyTitle("BetterModeration")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[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]
	[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;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace BetterModeration
{
	internal sealed class TimedBan
	{
		public ulong PlatformId { get; set; }

		public string LastKnownName { get; set; } = "Unknown";

		public string Reason { get; set; } = "Host moderation";

		public DateTime CreatedUtc { get; set; }

		public DateTime ExpiresUtc { get; set; }
	}
	internal static class BanStore
	{
		private static readonly object Sync = new object();

		private static readonly string DirectoryPath = Path.Combine(MelonEnvironment.UserDataDirectory, "BetterModeration");

		private static readonly string FilePath = Path.Combine(DirectoryPath, "timed-bans.json");

		private static List<TimedBan> _bans = new List<TimedBan>();

		internal static IReadOnlyList<TimedBan> Active
		{
			get
			{
				lock (Sync)
				{
					PurgeExpired(save: true);
					return _bans.ToArray();
				}
			}
		}

		internal static void Load()
		{
			lock (Sync)
			{
				Directory.CreateDirectory(DirectoryPath);
				try
				{
					_bans = (File.Exists(FilePath) ? (JsonConvert.DeserializeObject<List<TimedBan>>(File.ReadAllText(FilePath)) ?? new List<TimedBan>()) : new List<TimedBan>());
				}
				catch (Exception ex)
				{
					MelonLogger.Error("Better Moderation could not read its ban file: " + ex.Message);
					_bans = new List<TimedBan>();
				}
				PurgeExpired(save: true);
			}
		}

		internal static bool TryGet(ulong platformId, out TimedBan? ban)
		{
			lock (Sync)
			{
				PurgeExpired(save: true);
				ban = _bans.FirstOrDefault((TimedBan item) => item.PlatformId == platformId);
				return ban != null;
			}
		}

		internal static void Add(ulong platformId, string name, TimeSpan duration, string reason)
		{
			if (duration <= TimeSpan.Zero || duration > TimeSpan.FromDays(30.0))
			{
				throw new ArgumentOutOfRangeException("duration", "Timed bans must be between one minute and 30 days.");
			}
			lock (Sync)
			{
				DateTime utcNow = DateTime.UtcNow;
				_bans.RemoveAll((TimedBan item) => item.PlatformId == platformId);
				_bans.Add(new TimedBan
				{
					PlatformId = platformId,
					LastKnownName = (string.IsNullOrWhiteSpace(name) ? "Unknown" : name),
					Reason = (string.IsNullOrWhiteSpace(reason) ? "Host moderation" : reason),
					CreatedUtc = utcNow,
					ExpiresUtc = utcNow.Add(duration)
				});
				Save();
			}
		}

		internal static bool Remove(ulong platformId)
		{
			lock (Sync)
			{
				bool num = _bans.RemoveAll((TimedBan item) => item.PlatformId == platformId) != 0;
				if (num)
				{
					Save();
				}
				return num;
			}
		}

		private static void PurgeExpired(bool save)
		{
			int num = _bans.RemoveAll((TimedBan item) => item.ExpiresUtc <= DateTime.UtcNow);
			if (save && num != 0)
			{
				Save();
			}
		}

		private static void Save()
		{
			Directory.CreateDirectory(DirectoryPath);
			string text = FilePath + ".tmp";
			File.WriteAllText(text, JsonConvert.SerializeObject((object)_bans, (Formatting)1));
			File.Copy(text, FilePath, overwrite: true);
			File.Delete(text);
		}
	}
	internal sealed class Main : MelonMod
	{
		[HarmonyPatch(typeof(ConnectionRequestMessage), "OnHandleMessage")]
		[HarmonyPriority(800)]
		private static class ConnectionRequestGuard
		{
			private static bool Prefix(ReceivedMessage received)
			{
				if (!NetworkInfo.IsHost)
				{
					return true;
				}
				ConnectionRequestData val = ((ReceivedMessage)(ref received)).ReadData<ConnectionRequestData>();
				ulong? platformID = ((ReceivedMessage)(ref received)).PlatformID;
				if (!platformID.HasValue || !ModerationService.ShouldDeny(platformID.Value, val.BackupPlatformID, out string reason))
				{
					return true;
				}
				ConnectionSender.SendConnectionDeny(platformID.Value, reason);
				MelonLogger.Warning($"Rejected connection from authenticated platform ID {platformID.Value}: {reason}.");
				return false;
			}
		}

		[CompilerGenerated]
		private static class <>O
		{
			public static Action<NetworkLayer> <0>__PatchNativeUiAfterFusionLogin;

			public static ServerEvent <1>__StartNewSession;
		}

		private static bool _nativeUiPatched;

		public override void OnInitializeMelon()
		{
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Expected O, but got Unknown
			//IL_0055: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0060: Expected O, but got Unknown
			BanStore.Load();
			NetworkLayer.OnLoggedInEvent += PatchNativeUiAfterFusionLogin;
			object obj = <>O.<1>__StartNewSession;
			if (obj == null)
			{
				ServerEvent val = ModerationService.StartNewSession;
				<>O.<1>__StartNewSession = val;
				obj = (object)val;
			}
			MultiplayerHooking.OnJoinedServer += (ServerEvent)obj;
			object obj2 = <>O.<1>__StartNewSession;
			if (obj2 == null)
			{
				ServerEvent val2 = ModerationService.StartNewSession;
				<>O.<1>__StartNewSession = val2;
				obj2 = (object)val2;
			}
			MultiplayerHooking.OnDisconnected += (ServerEvent)obj2;
			MelonLogger.Msg("Better Moderation initialized; waiting for Fusion login before extending its Actions menu.");
		}

		private static void PatchNativeUiAfterFusionLogin(NetworkLayer _)
		{
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_0060: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Expected O, but got Unknown
			if (!_nativeUiPatched)
			{
				MethodInfo methodInfo = AccessTools.Method(typeof(MenuLocation), "ApplyPlayerToElement", (Type[])null, (Type[])null);
				MethodInfo methodInfo2 = AccessTools.Method(typeof(NativeFusionMenu), "Postfix", (Type[])null, (Type[])null);
				if (methodInfo == null || methodInfo2 == null)
				{
					MelonLogger.Error("Better Moderation could not locate Fusion's player Actions UI methods.");
					return;
				}
				new Harmony("BetterModeration.NativeFusionUi").Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				_nativeUiPatched = true;
				MelonLogger.Msg("Better Moderation added to Fusion's native player Actions menu after login.");
			}
		}
	}
	internal static class ModerationService
	{
		private static readonly Dictionary<ulong, string> SessionBans = new Dictionary<ulong, string>();

		internal static IReadOnlyDictionary<ulong, string> ActiveSessionBans => new Dictionary<ulong, string>(SessionBans);

		internal static void StartNewSession()
		{
			SessionBans.Clear();
			MelonLogger.Msg("Better Moderation: session-ban list cleared for the new server session.");
		}

		internal static bool IsSessionBanned(ulong platformId)
		{
			return SessionBans.ContainsKey(platformId);
		}

		internal static void Kick(PlayerID player)
		{
			if (CanModerate(player))
			{
				NetworkHelper.KickUser(player);
				MelonLogger.Warning($"Kicked {SafeName(player)} ({player.PlatformID}).");
			}
		}

		internal static void SessionBan(PlayerID player)
		{
			if (CanModerate(player))
			{
				SessionBans[player.PlatformID] = SafeName(player);
				NetworkHelper.KickUser(player);
				MelonLogger.Warning($"Session-banned {SafeName(player)} ({player.PlatformID}).");
			}
		}

		internal static void TimedBan(PlayerID player, TimeSpan duration)
		{
			if (CanModerate(player))
			{
				BanStore.Add(player.PlatformID, SafeName(player), duration, "Host moderation");
				NetworkHelper.KickUser(player);
				MelonLogger.Warning($"Timed-banned {SafeName(player)} ({player.PlatformID}) until {DateTime.UtcNow.Add(duration):O}.");
			}
		}

		internal static bool ShouldDeny(ulong authenticatedPlatformId, ulong claimedPlatformId, out string reason)
		{
			if (authenticatedPlatformId == 0L || authenticatedPlatformId != claimedPlatformId)
			{
				reason = "Identity verification failed";
				return true;
			}
			if (SessionBans.ContainsKey(authenticatedPlatformId))
			{
				reason = "Banned for this server session";
				return true;
			}
			if (BanStore.TryGet(authenticatedPlatformId, out TimedBan ban))
			{
				reason = $"Banned until {ban.ExpiresUtc:u}";
				return true;
			}
			reason = string.Empty;
			return false;
		}

		internal static bool ClearActions(ulong platformId)
		{
			bool num = SessionBans.Remove(platformId);
			bool flag = BanStore.Remove(platformId);
			if (num || flag)
			{
				MelonLogger.Msg($"Cleared moderation actions for platform ID {platformId}.");
			}
			return num || flag;
		}

		internal static string SafeName(PlayerID player)
		{
			try
			{
				string value = player.Metadata.Nickname.GetValue();
				string value2 = player.Metadata.Username.GetValue();
				string text = ((string.IsNullOrWhiteSpace(value) || value == "?") ? value2 : value);
				return string.IsNullOrWhiteSpace(text) ? "Unknown" : text;
			}
			catch
			{
				return "Unknown";
			}
		}

		private static bool CanModerate(PlayerID player)
		{
			if (!NetworkInfo.IsHost || player == null || !player.IsValid || player.IsHost)
			{
				MelonLogger.Warning("Better Moderation action ignored: only the host can moderate a valid non-host player.");
				return false;
			}
			return true;
		}
	}
	internal static class NativeFusionMenu
	{
		internal static void Postfix(PlayerElement element, PlayerID player)
		{
			//IL_009f: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bc: Unknown result type (might be due to invalid IL or missing references)
			//IL_0188: Unknown result type (might be due to invalid IL or missing references)
			//IL_01af: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)element == (Object)null || player == null || !player.IsValid || player.IsMe)
			{
				return;
			}
			GroupElement val = ((GroupElement)((element.ActionsElement.Pages.Count > 0) ? element.ActionsElement.Pages[0] : element.ActionsElement.AddPage())).AddElement<GroupElement>(BuildHeading(player.PlatformID));
			if (!NetworkInfo.IsHost || player.IsHost)
			{
				MenuChaining.WithInteractability<ButtonElement>(MenuChaining.WithColor<ButtonElement>(val.AddElement<ButtonElement>("Available to the lobby host"), Color.yellow), false);
				return;
			}
			MenuChaining.Do(MenuChaining.WithColor<FunctionElement>(val.AddElement<FunctionElement>("Ban for this session"), Color.red), (Action)delegate
			{
				ModerationService.SessionBan(player);
			});
			int minutes = 0;
			int hours = 0;
			int days = 0;
			MenuChaining.WithValue(MenuChaining.WithIncrement(MenuChaining.WithLimits(val.AddElement<IntElement>("Minutes"), 0, 59), 1), 0).OnValueChanged = delegate(int value)
			{
				minutes = value;
			};
			MenuChaining.WithValue(MenuChaining.WithIncrement(MenuChaining.WithLimits(val.AddElement<IntElement>("Hours"), 0, 23), 1), 0).OnValueChanged = delegate(int value)
			{
				hours = value;
			};
			MenuChaining.WithValue(MenuChaining.WithIncrement(MenuChaining.WithLimits(val.AddElement<IntElement>("Days"), 0, 30), 1), 0).OnValueChanged = delegate(int value)
			{
				days = value;
			};
			MenuChaining.Do(MenuChaining.WithColor<FunctionElement>(val.AddElement<FunctionElement>("Apply timed ban"), Color.red), (Action)delegate
			{
				ApplyTimedBan(player, days, hours, minutes);
			});
			MenuChaining.Do(MenuChaining.WithColor<FunctionElement>(val.AddElement<FunctionElement>("Clear this player's moderation"), Color.green), (Action)delegate
			{
				bool flag = ModerationService.ClearActions(player.PlatformID);
				Notify(flag ? "Moderation cleared" : "Nothing to clear", flag ? "This player's session and timed bans were removed." : "This player has no BetterModeration bans.", (NotificationType)(flag ? 3 : 0));
			});
		}

		private static void ApplyTimedBan(PlayerID player, int days, int hours, int minutes)
		{
			TimeSpan timeSpan = TimeSpan.FromDays(days) + TimeSpan.FromHours(hours) + TimeSpan.FromMinutes(minutes);
			if (timeSpan <= TimeSpan.Zero || timeSpan > TimeSpan.FromDays(30.0))
			{
				Notify("Invalid ban duration", "Choose at least one minute and no more than 30 days.", (NotificationType)2);
				return;
			}
			ModerationService.TimedBan(player, timeSpan);
			Notify("Timed ban applied", $"{ModerationService.SafeName(player)} was banned until {DateTime.Now.Add(timeSpan):g}.", (NotificationType)3);
		}

		private static string BuildHeading(ulong id)
		{
			bool flag = ModerationService.IsSessionBanned(id);
			TimedBan ban;
			bool flag2 = BanStore.TryGet(id, out ban);
			if (flag && flag2)
			{
				return $"BetterModeration (Session + until {ban.ExpiresUtc.ToLocalTime():g})";
			}
			if (flag)
			{
				return "BetterModeration (Session banned)";
			}
			if (flag2)
			{
				return $"BetterModeration (Banned until {ban.ExpiresUtc.ToLocalTime():g})";
			}
			return "BetterModeration";
		}

		private static void Notify(string title, string message, NotificationType type)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_004d: Expected O, but got Unknown
			Notifier.Send(new Notification
			{
				Title = NotificationText.op_Implicit(title),
				Message = NotificationText.op_Implicit(message),
				Type = type,
				PopupLength = 3f,
				SaveToMenu = true,
				ShowPopup = true,
				Tag = "BetterModeration"
			});
		}
	}
}