using System; using System.Collections.Generic; using Newtonsoft.Json; using UnityEngine; using System.Linq; using System.Text; //using Oxide.Core.Libraries.Covalence; //TODO: convert to universal plugin if requested... smh namespace Oxide.Plugins { [Info("Command Spam Blocker", "Khan", "1.0.0")] [Description("Prevents spamming of chat & console commands")] public class CommandSpamBlocker : RustPlugin { #region Fields private static Configuration _config; private const string Chat = "chat.say"; [Flags] private enum Type { All = Chat | Console, Chat = 1, Console = 2, } private static float _cd; private static float _cdm; private HashSet _commands = new HashSet(); private Hash _cooldowns = new Hash(); private static HashSet _ignore; #endregion #region Config private class Configuration { [JsonProperty("Add commands to block ( Must be lowercased do not add the slash! / ) ( 3 = Block Both | 1 = Block Chat | 2 = Block Console )")] public Hash Commands; [JsonProperty("Sets the time between command usage")] public float CoolDownTime = 3f; [JsonProperty("Sets the max accumulated cooldown time limit before kicking the player if they keep trying to spam")] public float CoolDownTimeMax = 120.0f; [JsonProperty("Exclude players from being blocked ( Add their steam 64 id's to the list )", ObjectCreationHandling = ObjectCreationHandling.Auto)] public HashSet Ignore; [JsonProperty("Message Responses")] public Lang Msg = new Lang(); public string ToJson() => JsonConvert.SerializeObject(this); public Dictionary ToDictionary() => JsonConvert.DeserializeObject>(ToJson()); } private class Lang { [JsonProperty("( Chat Icon ) Just insert a valid steam 64 ID to set")] public ulong ChatIcon = 0; public string CommandBlocked = "You are on command cooldown, try again in {0} seconds"; public string KickReason = "Kicked for excessive command spam!"; } protected override void LoadDefaultConfig() => _config = new Configuration(); protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) throw new JsonException(); if (!_config.ToDictionary().Keys.SequenceEqual(Config.ToDictionary(x => x.Key, x => x.Value).Keys)) { PrintWarning("Configuration appears to be outdated; Updating and saving"); SaveConfig(); } } catch { PrintError($"Configuration file {Name}.json is invalid; using defaults"); LoadDefaultConfig(); } } protected override void SaveConfig() { PrintToConsole($"Configuration changes saved to {Name}.json"); Config.WriteObject(_config, true); } private void Init() => Unsubscribe(nameof (OnPlayerCommand)); private void Loaded() { if (_config.Ignore.IsNullOrEmpty()) _config.Ignore = new HashSet(); if (_config.Commands.IsNullOrEmpty()) _config.Commands = new Hash { { "command", Type.All }, }; SaveConfig(); _ignore = _config.Ignore; if (!_ignore.IsNullOrEmpty()) { StringBuilder sb = new StringBuilder(); int count = 0; for (int i = 0; i < _config.Ignore.Count; i++) { var dingus = _config.Ignore.ElementAt(i); if (!dingus.IsSteamId()) { sb.Append($"line {i} {dingus}\n"); count++; } } PrintError($"{count} of {_config.Ignore.Count} steam ID's are not valid please double check and fix them!\n{sb}"); sb.Clear(); } _cd = _config.CoolDownTime; _cdm = _config.CoolDownTimeMax; var name = _config.Commands.First().Key; if (_config.Commands.Count == 1 && (name == "command" || string.IsNullOrEmpty(name))) return; foreach (var c in _config.Commands) if (!_commands.Contains(c.Key)) _commands.Add(c.Key); Subscribe(nameof (OnPlayerCommand)); } private void Unload() { _commands.Clear(); _ignore.Clear(); _config = null; } #endregion #region Oxide private void OnPlayerDisconnected(BasePlayer player) => _cooldowns.Remove(player.userID); private object OnPlayerCommand(BasePlayer player, string command, string[] args) => player == null ? null : _ignore.Contains(player.userID) ? null : CanDo(command, player); // Considering making this a universal plugin but am lazy AF.. + don't feel like making a lang file system for umod anymore.. //private object OnUserCommand(IPlayer iplayer, string command, string[] args) => iplayer == null ? null : _ignore.Contains(iplayer.userID) ? null : CanDo(command, player); private object OnServerCommand(ConsoleSystem.Arg arg) { BasePlayer player = arg.Player(); if (player != null && arg.cmd.FullName != Chat) { string command = arg.cmd.Name; string fullCommand = arg.cmd.FullName; if (!string.IsNullOrEmpty(arg.FullString)) { command += $" {arg.FullString}"; fullCommand += $" {arg.FullString}"; } foreach (var _cmd in _config.Commands) { if (_cmd.Value != Type.Console && _cmd.Value != Type.All) continue; if ((command.StartsWith(_cmd.Key, StringComparison.OrdinalIgnoreCase) || fullCommand.StartsWith(_cmd.Key, StringComparison.OrdinalIgnoreCase)) && HasCommandCooldown(player)) return true; } } return null; } #endregion #region Helpers private object CanDo(string command, BasePlayer player) { if (!_commands.Contains(command, StringComparer.OrdinalIgnoreCase)) return null; command = command.ToLower(); if ((_config.Commands[command].HasFlag(Type.Chat) || _config.Commands[command].HasFlag(Type.Console)) && HasCommandCooldown(player)) return true; return null; } private bool HasCommandCooldown(BasePlayer player) { float cooldown; if (!_cooldowns.TryGetValue(player.userID, out cooldown)) _cooldowns[player.userID] = cooldown = Time.realtimeSinceStartup - 30f; if (cooldown > Time.realtimeSinceStartup) { _cooldowns[player.userID] = cooldown += 1f; var remaining = cooldown - Time.realtimeSinceStartup; if (remaining <= _cdm) Response(player, _config.Msg.CommandBlocked, $"{remaining:0}"); else player.Kick(_config.Msg.KickReason); return true; } _cooldowns[player.userID] = Time.realtimeSinceStartup + _cd; return false; } private static void Response(BasePlayer player, string message, params object[] args) { if (player == null) return; ConsoleNetwork.SendClientCommand(player.Connection, "chat.add", 2, _config.Msg.ChatIcon, string.Format(message, args)); } #endregion } }