using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Libraries.Covalence; using Oxide.Core.Plugins; using UnityEngine; using System.Threading.Tasks; // ____ _ _ _ // / ___|(_) __ _(_) | ___ // \___ \| |/ _` | | |/ _ \ // ___) | | (_| | | | (_) | // |____/|_|\__, |_|_|\___/ // |___/ // ☽✧✵✧✵✧✵✧✵✧✵✧✵✧☾ // ✦ RUST PLUGINS ✦ // ✦ Sigilo.dev ✦ // ☽✧✵✧✵✧✵✧✵✧✵✧✵✧☾ namespace Oxide.Plugins { [Info("AutoReportBan", "Sigilo", "1.3.1")] [Description("Automatically bans players who receive multiple reports within a time window using Rust's native F7 report system")] class AutoReportBan : RustPlugin { #region Configuration private Configuration config; private class Configuration { [JsonProperty("Reports threshold")] public int ReportsThreshold = 10; [JsonProperty("Time window (minutes)")] public int TimeWindowMinutes = 30; [JsonProperty("Ban reason template")] public string BanReasonTemplate = "Banned by server"; [JsonProperty("Ban duration (minutes, 0 = permanent)")] public int BanDurationMinutes = 0; [JsonProperty("Debug mode")] public bool DebugMode = false; [JsonProperty("Discord Webhook URL")] public string WebhookUrl = ""; [JsonProperty("Report Discord Webhook URL")] public string ReportWebhookUrl = ""; [JsonProperty("Discord Bot Name")] public string DiscordBotName = "Auto Report Ban"; [JsonProperty("Server Name")] public string ServerName = "Rust Server"; [JsonProperty("Reporter cooldown after successful ban (minutes)")] public int ReporterCooldownMinutes = 60; } protected override void LoadConfig() { base.LoadConfig(); try { config = Config.ReadObject(); if (config == null) throw new Exception("Could not read config file"); SaveConfig(); PrintDebug("Configuration loaded successfully"); } catch (Exception ex) { PrintError($"Error loading config: {ex.Message}"); config = new Configuration(); } } protected override void LoadDefaultConfig() => config = new Configuration(); protected override void SaveConfig() => Config.WriteObject(config); #endregion #region Data Storage private class PlayerReport { public ulong targetId; public string reporterId; public string type; public string reason; public string description; public DateTime Timestamp; } private class StoredData { public Dictionary> PlayerReports = new Dictionary>(); public HashSet WhitelistedPlayers = new HashSet(); public Dictionary ReporterCooldowns = new Dictionary(); } private StoredData storedData; private Dictionary tempBans = new Dictionary(); private Timer cleanupTimer; private Timer tempBanTimer; private void SaveData() { if (storedData == null) { PrintDebug("Creating new StoredData before saving"); storedData = new StoredData(); } try { Interface.Oxide.DataFileSystem.WriteObject("AutoReportBan/data", storedData); PrintDebug($"Data saved to disk. Current player count in storage: {storedData.PlayerReports.Count}"); int totalReports = 0; foreach (var reports in storedData.PlayerReports.Values) { totalReports += reports.Count; } PrintDebug($"Total reports in storage: {totalReports}"); var dataCheck = Interface.Oxide.DataFileSystem.ReadObject("AutoReportBan/data"); if (dataCheck != null && dataCheck.PlayerReports != null) { int checkTotalReports = 0; foreach (var reports in dataCheck.PlayerReports.Values) { checkTotalReports += reports.Count; } PrintDebug($"Data verification - Read back {dataCheck.PlayerReports.Count} players with {checkTotalReports} total reports from storage"); } else { PrintError("WARNING: Data verification failed - could not read back data file"); } } catch (Exception ex) { PrintError($"Error saving data: {ex.Message}"); PrintError($"Stack trace: {ex.StackTrace}"); } } private void SaveTempBanData() { Interface.Oxide.DataFileSystem.WriteObject("AutoReportBan/tempbans", tempBans); } private void LoadData() { try { storedData = Interface.Oxide.DataFileSystem.ReadObject("AutoReportBan/data"); if (storedData == null) { PrintDebug("No existing data found, creating new data file"); storedData = new StoredData(); SaveData(); } else { PrintDebug($"Data loaded successfully. Found {storedData.PlayerReports.Count} players with reports"); } } catch (Exception ex) { PrintError($"Error loading data: {ex.Message}"); storedData = new StoredData(); } try { tempBans = Interface.Oxide.DataFileSystem.ReadObject>("AutoReportBan/tempbans"); if (tempBans == null) { PrintDebug("No existing temp ban data found, creating new temp ban file"); tempBans = new Dictionary(); SaveTempBanData(); } else { PrintDebug($"Temp ban data loaded successfully. Found {tempBans.Count} temporary bans"); } } catch (Exception ex) { PrintError($"Error loading temp ban data: {ex.Message}"); tempBans = new Dictionary(); } } #endregion #region Hooks private void Init() { LoadConfig(); LoadData(); AddCovalenceCommand("listreports", "ConsoleListReports", "autoreportban.admin"); AddCovalenceCommand("checkplayer", "ConsoleCheckPlayer", "autoreportban.admin"); AddCovalenceCommand("testreport", "ConsoleTestReport", "autoreportban.admin"); AddCovalenceCommand("testwebhook", "ConsoleTestWebhook", "autoreportban.admin"); AddCovalenceCommand("cleanreports", "ConsoleCleanReports", "autoreportban.admin"); AddCovalenceCommand("arb_addwl", "ConsoleWhitelistAdd", "autoreportban.admin"); AddCovalenceCommand("arb_removewl", "ConsoleWhitelistRemove", "autoreportban.admin"); AddCovalenceCommand("arb_listwl", "ConsoleWhitelistList", "autoreportban.admin"); cleanupTimer = timer.Every(300f, CleanupOldReports); tempBanTimer = timer.Every(60f, CheckTempBans); CleanupOldReports(); Puts("AutoReportBan initialized"); Subscribe("OnPlayerReported"); } private void Unload() { if (cleanupTimer != null && !cleanupTimer.Destroyed) cleanupTimer.Destroy(); if (tempBanTimer != null && !tempBanTimer.Destroyed) tempBanTimer.Destroy(); SaveData(); SaveTempBanData(); PrintDebug("Plugin unloaded, data saved to disk"); } private void ConsoleListReports(IPlayer player, string command, string[] args) { if (player == null) { if (storedData?.PlayerReports == null || storedData.PlayerReports.Count == 0) { Puts("No active reports in the system."); return; } Puts("Current Active Reports:"); foreach (var kvp in storedData.PlayerReports) { var reports = GetRecentReports(kvp.Key); if (reports.Count == 0) continue; BasePlayer targetPlayer = null; if (ulong.TryParse(kvp.Key, out ulong targetSteamId)) { targetPlayer = RelationshipManager.FindByID(targetSteamId); } string targetName = targetPlayer?.displayName ?? "Unknown"; var uniqueReporters = reports.Select(r => r.reporterId).Distinct().Count(); var groupedReports = reports .GroupBy(r => r.type) .ToDictionary(g => g.Key, g => g.Count()); Puts($"\n{targetName} [{kvp.Key}]:"); Puts($"Total reports: {reports.Count} from {uniqueReporters} unique reporters"); foreach (var group in groupedReports) { Puts($"- {group.Key}: {group.Value}"); } var oldestReport = reports.Min(r => r.Timestamp); var timeLeft = oldestReport.AddMinutes(config.TimeWindowMinutes) - DateTime.UtcNow; Puts($"Time until oldest report expires: {timeLeft.Minutes}m {timeLeft.Seconds}s"); } return; } if (!player.HasPermission("autoreportban.admin")) return; if (storedData?.PlayerReports == null || storedData.PlayerReports.Count == 0) { player.Reply("No active reports in the system."); return; } player.Reply("Current Active Reports:"); foreach (var kvp in storedData.PlayerReports) { var reports = GetRecentReports(kvp.Key); if (reports.Count == 0) continue; BasePlayer targetPlayer = null; if (ulong.TryParse(kvp.Key, out ulong targetSteamId)) { targetPlayer = RelationshipManager.FindByID(targetSteamId); } string targetName = targetPlayer?.displayName ?? "Unknown"; var uniqueReporters = reports.Select(r => r.reporterId).Distinct().Count(); var groupedReports = reports .GroupBy(r => r.type) .ToDictionary(g => g.Key, g => g.Count()); player.Reply($"\n{targetName} [{kvp.Key}]:"); player.Reply($"Total reports: {reports.Count} from {uniqueReporters} unique reporters"); foreach (var group in groupedReports) { player.Reply($"- {group.Key}: {group.Value}"); } var oldestReport = reports.Min(r => r.Timestamp); var timeLeft = oldestReport.AddMinutes(config.TimeWindowMinutes) - DateTime.UtcNow; player.Reply($"Time until oldest report expires: {timeLeft.Minutes}m {timeLeft.Seconds}s"); } } private void ConsoleCheckPlayer(IPlayer player, string command, string[] args) { if (player == null) { if (args.Length == 0) { Puts("Usage: checkplayer "); return; } string consoleTargetId = args[0]; BasePlayer consoleTargetPlayer = null; if (consoleTargetId.Length == 17 && ulong.TryParse(consoleTargetId, out ulong consoleSteamId)) { consoleTargetPlayer = RelationshipManager.FindByID(consoleSteamId); } else { IPlayer iPlayer = covalence.Players.FindPlayer(consoleTargetId); if (iPlayer != null && ulong.TryParse(iPlayer.Id, out ulong foundSteamId)) { consoleTargetPlayer = RelationshipManager.FindByID(foundSteamId); } } if (consoleTargetPlayer == null) { Puts($"Could not find player: {consoleTargetId}"); return; } string consolePlayerTargetId = consoleTargetPlayer.UserIDString; var consoleReports = GetRecentReports(consolePlayerTargetId); if (consoleReports.Count == 0) { Puts($"No active reports found for {consoleTargetPlayer.displayName}"); return; } Puts($"\nReports for {consoleTargetPlayer.displayName} [{consolePlayerTargetId}]:"); Puts($"Total active reports: {consoleReports.Count}"); var consoleGroupedReports = consoleReports .GroupBy(r => r.type) .ToDictionary(g => g.Key, g => g.Count()); foreach (var group in consoleGroupedReports) { Puts($"- {group.Key}: {group.Value}"); } var consoleUniqueReporters = consoleReports.Select(r => r.reporterId).Distinct().Count(); Puts($"Unique reporters: {consoleUniqueReporters}"); var consoleOldestReport = consoleReports.Min(r => r.Timestamp); var consoleTimeLeft = consoleOldestReport.AddMinutes(config.TimeWindowMinutes) - DateTime.UtcNow; Puts($"Time until oldest report expires: {consoleTimeLeft.Minutes}m {consoleTimeLeft.Seconds}s"); foreach (var consoleReport in consoleReports.OrderByDescending(r => r.Timestamp)) { BasePlayer consoleReporterPlayer = null; if (ulong.TryParse(consoleReport.reporterId, out ulong reporterSteamId)) { consoleReporterPlayer = RelationshipManager.FindByID(reporterSteamId); } string consoleReporterName = consoleReporterPlayer?.displayName ?? "Unknown"; Puts($"- {consoleReporterName}: {consoleReport.reason} ({(DateTime.UtcNow - consoleReport.Timestamp).Minutes}m ago)"); } return; } if (!player.HasPermission("autoreportban.admin")) return; if (args.Length == 0) { player.Reply("Usage: checkplayer "); return; } string targetIdentifier = args[0]; BasePlayer targetPlayer = null; if (targetIdentifier.Length == 17 && ulong.TryParse(targetIdentifier, out ulong steamId)) { targetPlayer = RelationshipManager.FindByID(steamId); } else { IPlayer iPlayer = covalence.Players.FindPlayer(targetIdentifier); if (iPlayer != null && ulong.TryParse(iPlayer.Id, out ulong foundSteamId)) { targetPlayer = RelationshipManager.FindByID(foundSteamId); } } if (targetPlayer == null) { player.Reply($"Could not find player: {targetIdentifier}"); return; } string targetId = targetPlayer.UserIDString; var reports = GetRecentReports(targetId); if (reports.Count == 0) { player.Reply($"No active reports found for {targetPlayer.displayName}"); return; } player.Reply($"\nReports for {targetPlayer.displayName} [{targetId}]:"); player.Reply($"Total active reports: {reports.Count}"); var groupedReports = reports .GroupBy(r => r.type) .ToDictionary(g => g.Key, g => g.Count()); foreach (var group in groupedReports) { player.Reply($"- {group.Key}: {group.Value}"); } var uniqueReporters = reports.Select(r => r.reporterId).Distinct().Count(); player.Reply($"Unique reporters: {uniqueReporters}"); var oldestReport = reports.Min(r => r.Timestamp); var timeLeft = oldestReport.AddMinutes(config.TimeWindowMinutes) - DateTime.UtcNow; player.Reply($"Time until oldest report expires: {timeLeft.Minutes}m {timeLeft.Seconds}s"); foreach (var report in reports.OrderByDescending(r => r.Timestamp)) { BasePlayer reporterPlayer = null; if (ulong.TryParse(report.reporterId, out ulong reporterSteamId)) { reporterPlayer = RelationshipManager.FindByID(reporterSteamId); } string reporterName = reporterPlayer?.displayName ?? "Unknown"; player.Reply($"- {reporterName}: {report.reason} ({(DateTime.UtcNow - report.Timestamp).Minutes}m ago)"); } } private void ConsoleTestReport(IPlayer player, string command, string[] args) { if (player != null && !player.HasPermission("autoreportban.admin")) return; if (args.Length < 2) { Puts("Usage: testreport "); return; } string reporterIdentifier = args[0]; string targetIdentifier = args[1]; IPlayer reporter = null; if (reporterIdentifier.Length == 17 && ulong.TryParse(reporterIdentifier, out _)) { reporter = covalence.Players.FindPlayerById(reporterIdentifier); } else { reporter = covalence.Players.FindPlayer(reporterIdentifier); } if (reporter == null) { Puts($"Could not find reporter: {reporterIdentifier}"); return; } IPlayer target = null; if (targetIdentifier.Length == 17 && ulong.TryParse(targetIdentifier, out _)) { target = covalence.Players.FindPlayerById(targetIdentifier); } else { target = covalence.Players.FindPlayer(targetIdentifier); } if (target == null) { Puts($"Could not find target: {targetIdentifier}"); return; } Puts($"Creating test report: {reporter.Name} reporting {target.Name}"); ProcessPlayerReport(reporter, target, "[cheat] Test report from console", "Test description text"); Puts("Test report created and processed"); var reports = GetRecentReports(target.Id); Puts($"Current reports for {target.Name}: {reports.Count}"); } private void ConsoleTestWebhook(IPlayer player, string command, string[] args) { if (player != null && !player.HasPermission("autoreportban.admin")) return; bool hasMainWebhook = !string.IsNullOrEmpty(config.WebhookUrl); bool hasReportWebhook = !string.IsNullOrEmpty(config.ReportWebhookUrl); if (!hasMainWebhook && !hasReportWebhook) { string message = player != null ? "Error: No Discord webhook URLs are configured. Please set at least one in the config file." : "Error: No Discord webhook URLs are configured. Please set at least one in the config file."; if (player != null) player.Reply(message); else Puts(message); return; } var responseBuilder = new System.Text.StringBuilder("Testing Discord webhook(s)..\n"); if (hasMainWebhook) { responseBuilder.AppendLine("- Main webhook (bans): Sending test message"); var banFields = new List> { new Dictionary { {"name", "Test Field 1"}, {"value", "This is a test field"}, {"inline", true} }, new Dictionary { {"name", "Test Field 2"}, {"value", "This is another test field"}, {"inline", true} }, new Dictionary { {"name", "Server Info"}, {"value", $"Players Online: {BasePlayer.activePlayerList.Count}"}, {"inline", false} }, new Dictionary { {"name", "Server"}, {"value", config.ServerName}, {"inline", false} } }; SendDiscordEmbed("Test Ban Webhook", "This is a test message from the AutoReportBan plugin (Ban webhook).", 3447003, banFields); } if (hasReportWebhook) { responseBuilder.AppendLine("- Report webhook (F7 reports): Sending test message"); var reportFields = new List> { new Dictionary { {"name", "Reported Player"}, {"value", $"[TestPlayer](https://steamcommunity.com/profiles/76561198000000000) [76561198000000000]"}, {"inline", true} }, new Dictionary { {"name", "Reporter"}, {"value", $"[TestReporter](https://steamcommunity.com/profiles/76561198000000001) [76561198000000001]"}, {"inline", true} }, new Dictionary { {"name", "Report Type"}, {"value", "cheat"}, {"inline", true} }, new Dictionary { {"name", "Report Reason"}, {"value", "[cheat] This is a test report reason"}, {"inline", false} }, new Dictionary { {"name", "Current Status"}, {"value", $"1/{config.ReportsThreshold} unique reporters (1 total reports) in the last {config.TimeWindowMinutes} minutes"}, {"inline", false} }, new Dictionary { {"name", "Server"}, {"value", config.ServerName}, {"inline", false} } }; SendDiscordEmbedToUrl("Test Report Webhook", "This is a test message from the AutoReportBan plugin (Report webhook).", 16750848, reportFields, config.ReportWebhookUrl); } responseBuilder.AppendLine("Test webhook(s) sent. Please check your Discord channel(s)."); string finalResponse = responseBuilder.ToString(); if (player != null) player.Reply(finalResponse); else Puts(finalResponse); } private void ConsoleCleanReports(IPlayer player, string command, string[] args) { if (player != null && !player.HasPermission("autoreportban.admin")) return; player.Reply("Manually triggering cleanup of expired reports..."); CleanupOldReports(); player.Reply("Cleanup complete. Check console for details."); } private void CleanupOldReports() { if (storedData?.PlayerReports == null) return; PrintDebug("Starting cleanup of old reports..."); var expiredCooldowns = storedData.ReporterCooldowns.Where(cd => cd.Value < DateTime.UtcNow).ToList(); if (expiredCooldowns.Count > 0) { PrintDebug($"Cleaning up {expiredCooldowns.Count} expired reporter cooldowns."); foreach (var entry in expiredCooldowns) { storedData.ReporterCooldowns.Remove(entry.Key); } } DateTime cutoffTime = DateTime.UtcNow.AddMinutes(-config.TimeWindowMinutes); int cleanedReports = 0; var keysToRemove = new List(); foreach (var playerID in storedData.PlayerReports.Keys) { var reports = storedData.PlayerReports[playerID]; int oldCount = reports.Count; var expiredReports = reports.Where(r => r.Timestamp < cutoffTime).ToList(); if (expiredReports.Count > 0) { PrintDebug($"Found {expiredReports.Count} expired reports for player {playerID}"); foreach (var report in expiredReports) { reports.Remove(report); } cleanedReports += expiredReports.Count; if (reports.Count == 0) { keysToRemove.Add(playerID); PrintDebug($"All reports expired for player {playerID}, marking for removal"); } } } foreach (var key in keysToRemove) { storedData.PlayerReports.Remove(key); PrintDebug($"Removed player {key} from storage (all reports expired)"); } if (cleanedReports > 0 || keysToRemove.Count > 0) { PrintDebug($"Cleanup complete: Removed {cleanedReports} expired reports from {keysToRemove.Count} players"); SaveData(); } else { PrintDebug("No expired reports found during cleanup"); } } private void CheckTempBans() { if (tempBans.Count == 0) return; var now = DateTime.Now; var expiredBans = tempBans.Where(entry => entry.Value <= now).ToList(); if (expiredBans.Count > 0) { PrintDebug($"Checking temporary bans: Found {expiredBans.Count} expired bans to remove"); foreach (var entry in expiredBans) { try { ulong steamId = entry.Key; string unbanCommand = $"unban {steamId}"; PrintDebug($"Executing unban command: {unbanCommand}"); ConsoleSystem.Run(ConsoleSystem.Option.Server, unbanCommand); var targetName = covalence.Players.FindPlayerById(steamId.ToString())?.Name ?? "Unknown"; Puts($"Temporary ban expired: Unbanned player {targetName} [{steamId}]"); tempBans.Remove(steamId); if (!string.IsNullOrEmpty(config.WebhookUrl)) { var fields = new List> { new Dictionary { {"name", "Player"}, {"value", $"{targetName} [{steamId}]"}, {"inline", true} }, new Dictionary { {"name", "Ban Duration"}, {"value", $"{config.BanDurationMinutes} minutes"}, {"inline", true} }, new Dictionary { {"name", "Unbanned At"}, {"value", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}, {"inline", false} } }; SendDiscordEmbed("Temporary Ban Expired", "A player's temporary ban has expired and they have been unbanned.", 3447003, fields); } } catch (Exception ex) { PrintError($"Error removing temporary ban: {ex.Message}"); } } SaveTempBanData(); } } private void PrintDebug(string message) { if (config.DebugMode) { Puts($"[DEBUG] {message}"); } } private async Task SendDiscordEmbed(string title, string description, int color, List> fields) { if (string.IsNullOrEmpty(config.WebhookUrl)) return; await SendDiscordEmbedToUrl(title, description, color, fields, config.WebhookUrl); } private async Task SendDiscordEmbedToUrl(string title, string description, int color, List> fields, string webhookUrl) { if (string.IsNullOrEmpty(webhookUrl)) return; var embed = new Dictionary { {"title", title}, {"description", description}, {"color", color}, {"timestamp", DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")} }; if (fields != null && fields.Count > 0) { embed["fields"] = fields; } var payloadObj = new Dictionary { {"username", config.DiscordBotName}, {"embeds", new object[] { embed } } }; var payload = JsonConvert.SerializeObject(payloadObj); try { webrequest.EnqueuePost( webhookUrl.TrimEnd('/'), payload, (code, response) => { if (code != 204 && code != 200) PrintError($"Discord webhook error: {code} - {response}"); else PrintDebug("Discord webhook notification sent successfully"); }, this, new Dictionary { ["Content-Type"] = "application/json" } ); } catch (Exception ex) { PrintError($"Discord embed log failed: {ex.Message}"); } } private class ReportData { public string PlayerId; public string PlayerName; public string TargetId; public string TargetName; public string Subject; public string Message; public string Type; } [HookMethod("OnPlayerReported")] private void OnPlayerReport(string reportJson) { try { PrintDebug($"Raw report received: {reportJson}"); if (string.IsNullOrEmpty(reportJson)) { PrintDebug("Report JSON is null or empty"); return; } var report = JsonConvert.DeserializeObject(reportJson); if (report == null) { PrintDebug("Failed to deserialize report JSON"); return; } PrintDebug($"Deserialized report - Reporter: {report.PlayerName}[{report.PlayerId}], Target: {report.TargetName}[{report.TargetId}], Type: {report.Type}, Message: {report.Message}"); var covReporter = covalence.Players.FindPlayerById(report.PlayerId); var covReported = covalence.Players.FindPlayerById(report.TargetId); if (covReporter == null) { PrintDebug($"Could not find reporter with ID {report.PlayerId} using Covalence"); return; } if (covReported == null) { PrintDebug($"Could not find target player with ID {report.TargetId} using Covalence"); return; } PrintDebug($"Found players via Covalence - Reporter: {covReporter.Name}, Target: {covReported.Name}"); ProcessPlayerReport(covReporter, covReported, report.Subject, report.Message); PrintDebug($"Report successfully processed and stored"); var storedReports = GetRecentReports(report.TargetId); PrintDebug($"Current stored reports for {report.TargetName}: {storedReports.Count}"); SaveData(); PrintDebug("Data saved after processing report"); } catch (Exception ex) { PrintError($"Error processing report: {ex.Message}"); PrintError($"Stack trace: {ex.StackTrace}"); } } private void ProcessPlayerReport(IPlayer reporter, IPlayer reportedPlayer, string reportReason, string description = "") { try { if (reporter == null || reportedPlayer == null) { PrintDebug("ProcessPlayerReport: Reporter or reported player is null"); return; } if (storedData.WhitelistedPlayers.Contains(reportedPlayer.Id)) { PrintDebug($"Report against {reportedPlayer.Name} blocked because they are whitelisted."); return; } if (storedData.ReporterCooldowns.TryGetValue(reporter.Id, out DateTime cooldownExpiry) && cooldownExpiry > DateTime.UtcNow) { var timeLeft = cooldownExpiry - DateTime.UtcNow; Puts($"BLOCKED: {reporter.Name} is on a reporter cooldown for another {timeLeft.Minutes}m {timeLeft.Seconds}s."); return; } string reportType = DetermineReportType(reportReason); string cleanedReason = CleanReportReason(reportReason); string reportedID = reportedPlayer.Id; string reporterID = reporter.Id; Puts($"{reporter.Name} reported {reportedPlayer.Name} for {reportReason}"); if (storedData == null) { PrintDebug("StoredData was null, initializing new instance"); storedData = new StoredData(); } if (!storedData.PlayerReports.ContainsKey(reportedID)) { PrintDebug($"Creating new report list for {reportedPlayer.Name}"); storedData.PlayerReports[reportedID] = new List(); } var recentReports = GetRecentReports(reportedID); if (recentReports.Any(r => r.reporterId == reporterID)) { Puts($"BLOCKED: {reporter.Name} attempted to report {reportedPlayer.Name} again within the {config.TimeWindowMinutes} minute window"); return; } var report = new PlayerReport { targetId = ulong.Parse(reportedID), reporterId = reporterID, type = reportType, reason = cleanedReason, description = description, Timestamp = DateTime.UtcNow }; storedData.PlayerReports[reportedID].Add(report); PrintDebug($"Report added - Current reports for {reportedPlayer.Name}: {storedData.PlayerReports[reportedID].Count}"); var currentReports = GetRecentReports(reportedID); var uniqueReporters = currentReports.Select(r => r.reporterId).Distinct().Count(); Puts($"Current status for {reportedPlayer.Name}: {uniqueReporters} unique reporters in the last {config.TimeWindowMinutes} minutes"); SaveData(); if (!string.IsNullOrEmpty(config.ReportWebhookUrl)) { SendReportToDiscord(reporter, reportedPlayer, cleanedReason, reportType, description, uniqueReporters, currentReports.Count); } CheckReportThreshold(reportedID); } catch (Exception ex) { PrintError($"Error in ProcessPlayerReport: {ex.Message}"); PrintError($"Stack trace: {ex.StackTrace}"); } } private string DetermineReportType(string reportReason) { string lowerReason = reportReason.ToLower(); if (lowerReason.StartsWith("[cheat") || lowerReason.Contains("hack") || lowerReason.Contains("aim")) return "cheat"; if (lowerReason.StartsWith("[abuse") || lowerReason.StartsWith("[abusive") || lowerReason.Contains("exploit")) return "abuse"; if (lowerReason.StartsWith("[name") || lowerReason.Contains("offensive")) return "name"; if (lowerReason.StartsWith("[grief") || lowerReason.Contains("block")) return "grief"; return "other"; } private string CleanReportReason(string reportReason) { if (reportReason.StartsWith("[")) { int closeBracketIndex = reportReason.IndexOf("]"); if (closeBracketIndex > 0 && closeBracketIndex + 1 < reportReason.Length) { return reportReason.Substring(closeBracketIndex + 1).Trim(); } } return reportReason; } private List GetRecentReports(string playerID) { List reportsList; if (!storedData.PlayerReports.TryGetValue(playerID, out reportsList)) { return new List(); } DateTime cutoffTime = DateTime.UtcNow.AddMinutes(-config.TimeWindowMinutes); return reportsList .Where(r => r.Timestamp >= cutoffTime) .ToList(); } private void CheckReportThreshold(string reportedID) { PrintDebug($"Checking report threshold for player ID: {reportedID}"); var recentReports = GetRecentReports(reportedID); var uniqueReporterIds = recentReports.Select(r => r.reporterId).Distinct().ToList(); var uniqueReportersCount = uniqueReporterIds.Count; PrintDebug($"Found {recentReports.Count} total reports from {uniqueReportersCount} unique reporters"); if (uniqueReportersCount < config.ReportsThreshold) { PrintDebug($"Threshold check: Not enough unique reporters ({uniqueReportersCount}/{config.ReportsThreshold})"); return; } PrintDebug($"Threshold reached! Proceeding with ban for player ID: {reportedID}"); IPlayer reportedPlayer = covalence.Players.FindPlayerById(reportedID); string playerName = reportedPlayer?.Name ?? "Unknown"; string banReason = config.BanReasonTemplate; string banDurationText = config.BanDurationMinutes > 0 ? $"{config.BanDurationMinutes} minutes" : "Permanent"; try { ulong steamId; if (!ulong.TryParse(reportedID, out steamId)) { PrintError($"Failed to parse Steam ID from {reportedID}"); return; } PrintDebug($"Parsed Steam ID: {steamId} from player ID: {reportedID}"); if (config.BanDurationMinutes > 0) { tempBans[steamId] = DateTime.Now.AddMinutes(config.BanDurationMinutes); SaveTempBanData(); string banCommand = $"banid {steamId} {config.BanDurationMinutes * 60} \"{banReason}\""; PrintDebug($"Executing temporary ban command: {banCommand}"); ConsoleSystem.Run(ConsoleSystem.Option.Server, banCommand); Puts($"AUTO-BANNED {playerName} [{steamId}] for {banDurationText}"); } else { string banCommand = $"banid {steamId} 0 \"{banReason}\""; PrintDebug($"Executing permanent ban command: {banCommand}"); ConsoleSystem.Run(ConsoleSystem.Option.Server, banCommand); Puts($"AUTO-BANNED {playerName} [{steamId}] PERMANENTLY with reason: {banReason}"); } var basePlayer = RelationshipManager.FindByID(steamId); if (basePlayer != null) { basePlayer.Kick(banReason); Puts($"Kicked {playerName} from the server"); } LogToFile("bans", $"{DateTime.Now} - Player: {playerName} [{steamId}] - {banReason} - Ban duration: {banDurationText}", this); if (storedData.PlayerReports.ContainsKey(reportedID)) { storedData.PlayerReports[reportedID].Clear(); PrintDebug($"Cleared reports for banned player {playerName}"); SaveData(); } if (!string.IsNullOrEmpty(config.WebhookUrl)) { var reportTypes = recentReports.Select(r => r.type).Distinct(); string reportTypesText = string.Join(", ", reportTypes); var recentReportDetails = recentReports .OrderByDescending(r => r.Timestamp) .Take(5) .Select(r => { var reporterName = covalence.Players.FindPlayerById(r.reporterId)?.Name ?? "Unknown"; return $"• {reporterName}: {r.type} - {r.reason}"; }) .ToList(); string recentReportsText = recentReportDetails.Count > 0 ? string.Join("\n", recentReportDetails) : "No detailed report information available"; var fields = new List> { new Dictionary { {"name", "Player"}, {"value", $"{playerName} [{steamId}]"}, {"inline", true} }, new Dictionary { {"name", "Report Count"}, {"value", $"{uniqueReportersCount} unique reporters"}, {"inline", true} }, new Dictionary { {"name", "Ban Duration"}, {"value", banDurationText}, {"inline", true} }, new Dictionary { {"name", "Report Types"}, {"value", reportTypesText}, {"inline", false} }, new Dictionary { {"name", "Recent Reports"}, {"value", recentReportsText}, {"inline", false} }, new Dictionary { {"name", "Ban Reason"}, {"value", banReason}, {"inline", false} }, new Dictionary { {"name", "Server"}, {"value", config.ServerName}, {"inline", false} } }; SendDiscordEmbed("Player Auto-Banned", $"A player has been automatically banned after receiving multiple reports.", 15158332, fields); } if (config.ReporterCooldownMinutes > 0) { var cooldownTime = DateTime.UtcNow.AddMinutes(config.ReporterCooldownMinutes); foreach(var reporterId in uniqueReporterIds) { storedData.ReporterCooldowns[reporterId] = cooldownTime; } PrintDebug($"Applied a {config.ReporterCooldownMinutes} minute reporting cooldown to {uniqueReporterIds.Count} players."); } } catch (Exception ex) { PrintError($"Error issuing ban: {ex.Message}"); PrintError($"Stack trace: {ex.StackTrace}"); } } private void OnPlayerReported(BasePlayer reporter, string targetId, string reason, string type) { PrintDebug($"DIRECT HOOK: Player {reporter.displayName} reported {targetId} for {reason} (type: {type})"); try { string actualTargetId = reason; string actualReason = type; PrintDebug($"Corrected parameters - Target ID: {actualTargetId}, Reason: {actualReason}"); IPlayer covReporter = reporter.IPlayer; IPlayer covTarget = covalence.Players.FindPlayerById(actualTargetId); if (covReporter == null) { PrintDebug($"Could not get IPlayer for reporter {reporter.displayName}"); return; } if (covTarget == null) { PrintDebug($"Could not find target player with ID {actualTargetId}"); return; } PrintDebug($"Processing direct hook report: {covReporter.Name} reporting {covTarget.Name} for {actualReason}"); ProcessPlayerReport(covReporter, covTarget, actualReason); var storedReports = GetRecentReports(actualTargetId); PrintDebug($"Current stored reports for {covTarget.Name}: {storedReports.Count}"); SaveData(); } catch (Exception ex) { PrintError($"Error processing direct hook report: {ex.Message}"); PrintError($"Stack trace: {ex.StackTrace}"); } } private void SendReportToDiscord(IPlayer reporter, IPlayer reportedPlayer, string reason, string reportType, string description, int uniqueReporters, int totalReports) { try { if (string.IsNullOrEmpty(config.ReportWebhookUrl)) return; ulong reportedSteamId; if (!ulong.TryParse(reportedPlayer.Id, out reportedSteamId)) { PrintDebug($"Failed to parse Steam ID from {reportedPlayer.Id}"); return; } ulong reporterSteamId; if (!ulong.TryParse(reporter.Id, out reporterSteamId)) { PrintDebug($"Failed to parse Steam ID from {reporter.Id}"); return; } string reportedSteamProfile = $"https://steamcommunity.com/profiles/{reportedSteamId}"; string reporterSteamProfile = $"https://steamcommunity.com/profiles/{reporterSteamId}"; var fields = new List> { new Dictionary { {"name", "Server"}, {"value", config.ServerName}, {"inline", false} }, new Dictionary { {"name", "Reported Player"}, {"value", $"[{reportedPlayer.Name}]({reportedSteamProfile}) [{reportedSteamId}]"}, {"inline", true} }, new Dictionary { {"name", "Reporter"}, {"value", $"[{reporter.Name}]({reporterSteamProfile}) [{reporterSteamId}]"}, {"inline", true} }, new Dictionary { {"name", "Report Type"}, {"value", reportType}, {"inline", false} }, new Dictionary { {"name", "Report Reason"}, {"value", reason}, {"inline", false} } }; if (!string.IsNullOrEmpty(description)) { fields.Add(new Dictionary { {"name", "Description"}, {"value", description}, {"inline", false} }); } fields.Add(new Dictionary { {"name", "Current Status"}, {"value", $"{uniqueReporters}/{config.ReportsThreshold} unique reporters ({totalReports} total reports) in the last {config.TimeWindowMinutes} minutes"}, {"inline", false} }); if (uniqueReporters >= config.ReportsThreshold) { var color = 15158332; SendDiscordEmbedToUrl( "Player Report - Ban Threshold Reached", $"A player has received a new report and has reached the auto-ban threshold. They have been banned from the server.", color, fields, config.ReportWebhookUrl ); } else if (uniqueReporters >= config.ReportsThreshold - 2 && uniqueReporters < config.ReportsThreshold) { var color = 16776960; SendDiscordEmbedToUrl( "Player Report - Approaching Threshold", $"A player has received a new report and is approaching the auto-ban threshold.", color, fields, config.ReportWebhookUrl ); } else { var color = 3447003; SendDiscordEmbedToUrl( "Player Report Received", "", color, fields, config.ReportWebhookUrl ); } } catch (Exception ex) { PrintError($"Error sending report to Discord: {ex.Message}"); } } private void ConsoleWhitelistAdd(IPlayer player, string command, string[] args) { if (player != null && !player.HasPermission("autoreportban.admin")) return; if (args.Length == 0) { (player ?? (IPlayer)covalence.Server).Reply("Usage: arb_addwl "); return; } IPlayer target = covalence.Players.FindPlayer(args[0]); if (target == null) { (player ?? (IPlayer)covalence.Server).Reply($"Player not found: {args[0]}"); return; } if (storedData.WhitelistedPlayers.Contains(target.Id)) { (player ?? (IPlayer)covalence.Server).Reply($"{target.Name} is already on the whitelist."); return; } storedData.WhitelistedPlayers.Add(target.Id); SaveData(); (player ?? (IPlayer)covalence.Server).Reply($"{target.Name} [{target.Id}] has been added to the auto-ban whitelist."); } private void ConsoleWhitelistRemove(IPlayer player, string command, string[] args) { if (player != null && !player.HasPermission("autoreportban.admin")) return; if (args.Length == 0) { (player ?? (IPlayer)covalence.Server).Reply("Usage: arb_removewl "); return; } IPlayer target = covalence.Players.FindPlayer(args[0]); if (target == null) { if (args[0].Length == 17 && ulong.TryParse(args[0], out _)) { if (storedData.WhitelistedPlayers.Remove(args[0])) { SaveData(); (player ?? (IPlayer)covalence.Server).Reply($"SteamID {args[0]} has been removed from the auto-ban whitelist."); } else { (player ?? (IPlayer)covalence.Server).Reply($"SteamID {args[0]} was not found on the whitelist."); } } else { (player ?? (IPlayer)covalence.Server).Reply($"Player not found: {args[0]}"); } return; } if (!storedData.WhitelistedPlayers.Contains(target.Id)) { (player ?? (IPlayer)covalence.Server).Reply($"{target.Name} is not on the whitelist."); return; } storedData.WhitelistedPlayers.Remove(target.Id); SaveData(); (player ?? (IPlayer)covalence.Server).Reply($"{target.Name} [{target.Id}] has been removed from the auto-ban whitelist."); } private void ConsoleWhitelistList(IPlayer player, string command, string[] args) { if (player != null && !player.HasPermission("autoreportban.admin")) return; var target = player ?? (IPlayer)covalence.Server; if (storedData.WhitelistedPlayers.Count == 0) { target.Reply("The auto-ban whitelist is empty."); return; } target.Reply("Whitelisted Players:"); foreach (var steamId in storedData.WhitelistedPlayers) { var whitelistedPlayer = covalence.Players.FindPlayerById(steamId); target.Reply($"- {whitelistedPlayer?.Name ?? "Unknown (Offline)"} [{steamId}]"); } } #endregion } }