C#:
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<Configuration>();
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<string, List<PlayerReport>> PlayerReports = new Dictionary<string, List<PlayerReport>>();
public HashSet<string> WhitelistedPlayers = new HashSet<string>();
public Dictionary<string, DateTime> ReporterCooldowns = new Dictionary<string, DateTime>();
}
private StoredData storedData;
private Dictionary<ulong, DateTime> tempBans = new Dictionary<ulong, DateTime>();
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<StoredData>("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<StoredData>("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<Dictionary<ulong, DateTime>>("AutoReportBan/tempbans");
if (tempBans == null)
{
PrintDebug("No existing temp ban data found, creating new temp ban file");
tempBans = new Dictionary<ulong, DateTime>();
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<ulong, DateTime>();
}
}
#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 <name/steamid>");
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 <name/steamid>");
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 <reporter_id/name> <target_id/name>");
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<Dictionary<string, object>>
{
new Dictionary<string, object>
{
{"name", "Test Field 1"},
{"value", "This is a test field"},
{"inline", true}
},
new Dictionary<string, object>
{
{"name", "Test Field 2"},
{"value", "This is another test field"},
{"inline", true}
},
new Dictionary<string, object>
{
{"name", "Server Info"},
{"value", $"Players Online: {BasePlayer.activePlayerList.Count}"},
{"inline", false}
},
new Dictionary<string, object>
{
{"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<Dictionary<string, object>>
{
new Dictionary<string, object>
{
{"name", "Reported Player"},
{"value", $"[TestPlayer](https://steamcommunity.com/profiles/76561198000000000) [76561198000000000]"},
{"inline", true}
},
new Dictionary<string, object>
{
{"name", "Reporter"},
{"value", $"[TestReporter](https://steamcommunity.com/profiles/76561198000000001) [76561198000000001]"},
{"inline", true}
},
new Dictionary<string, object>
{
{"name", "Report Type"},
{"value", "cheat"},
{"inline", true}
},
new Dictionary<string, object>
{
{"name", "Report Reason"},
{"value", "[cheat] This is a test report reason"},
{"inline", false}
},
new Dictionary<string, object>
{
{"name", "Current Status"},
{"value", $"1/{config.ReportsThreshold} unique reporters (1 total reports) in the last {config.TimeWindowMinutes} minutes"},
{"inline", false}
},
new Dictionary<string, object>
{
{"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<string>();
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<Dictionary<string, object>>
{
new Dictionary<string, object>
{
{"name", "Player"},
{"value", $"{targetName} [{steamId}]"},
{"inline", true}
},
new Dictionary<string, object>
{
{"name", "Ban Duration"},
{"value", $"{config.BanDurationMinutes} minutes"},
{"inline", true}
},
new Dictionary<string, object>
{
{"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<Dictionary<string, object>> 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<Dictionary<string, object>> fields, string webhookUrl)
{
if (string.IsNullOrEmpty(webhookUrl)) return;
var embed = new Dictionary<string, object>
{
{"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<string, object>
{
{"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<string, string>
{
["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<ReportData>(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<PlayerReport>();
}
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<PlayerReport> GetRecentReports(string playerID)
{
List<PlayerReport> reportsList;
if (!storedData.PlayerReports.TryGetValue(playerID, out reportsList))
{
return new List<PlayerReport>();
}
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<Dictionary<string, object>>
{
new Dictionary<string, object>
{
{"name", "Player"},
{"value", $"{playerName} [{steamId}]"},
{"inline", true}
},
new Dictionary<string, object>
{
{"name", "Report Count"},
{"value", $"{uniqueReportersCount} unique reporters"},
{"inline", true}
},
new Dictionary<string, object>
{
{"name", "Ban Duration"},
{"value", banDurationText},
{"inline", true}
},
new Dictionary<string, object>
{
{"name", "Report Types"},
{"value", reportTypesText},
{"inline", false}
},
new Dictionary<string, object>
{
{"name", "Recent Reports"},
{"value", recentReportsText},
{"inline", false}
},
new Dictionary<string, object>
{
{"name", "Ban Reason"},
{"value", banReason},
{"inline", false}
},
new Dictionary<string, object>
{
{"name", "Server"},
{"value", config.ServerName},
{"inline", false}
}
};
SendDiscordEmbed("Player Auto-Banned", $"A player has been automatically banned after r
// ... [Code preview truncated due to length. Full source in attached file] ...