// Requires: Clans Reborn // https://chaoscode.io/resources/clans-reborn.14/ using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Libraries.Covalence; using Oxide.Core.Plugins; namespace Oxide.Plugins { [Info("Clan Rename Logs", "SeesAll", "1.0.2")] [Description("Logs and audits clan rename actions for Clans Reborn, with reasons, per-wipe cleanup, and optional Discord logging.")] public class ClanRenameLogs : RustPlugin { private const string PermUse = "clanrenamelogs.use"; private const string DataFileName = "ClanRenameLogs"; [PluginReference] private Plugin Clans; private List _log = new List(); private Configuration _config; #region Config private class DiscordSettings { public bool Enabled = false; public string WebhookUrl = ""; public string Username = "ClanRenamer"; public string AvatarUrl = ""; public bool MentionHere = false; public string MentionRoleId = ""; } private class Configuration { public bool WipeLogsOnServerWipe = true; public DiscordSettings Discord = new DiscordSettings(); } protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) { throw new Exception("Config is null"); } } catch { PrintWarning("Configuration file is invalid or missing, creating a new one with default values."); LoadDefaultConfig(); } } protected override void LoadDefaultConfig() { _config = new Configuration(); SaveConfig(); } protected override void SaveConfig() { Config.WriteObject(_config, true); } #endregion #region Lang protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { ["Error.NoPermission"] = "You do not have permission to use this command.", ["Error.NoClansPlugin"] = "Clans/ClansReborn plugin not found. Cannot rename clans.", ["Usage.Chat"] = "Usage: /renameclan OLD_TAG NEW_TAG REASON", ["Usage.ChatExample"] = "Example: /renameclan BAD GOOD Offensive name", ["Usage.Console"] = "Usage: renameclan OLD_TAG NEW_TAG REASON", ["Error.EmptyTags"] = "Old and new clan tags cannot be empty.", ["Error.ClanNotFound"] = "No clan with tag '{0}' was found. Rename aborted.", ["Warn.CantVerifyClan"] = "Warning: Could not verify if the clan exists via Clans API.", ["Warn.NewTagExists"] = "Warning: A clan with tag '{0}' already exists.", ["Warn.RenameFailed"] = "Rename may have failed or been rejected by Clans Reborn. Please verify manually.", ["Info.LoggedRename"] = "Clan tag '{0}' has been renamed to '{1}' with reason: {2}.", ["Info.LoggedRenameConsole"] = "Clan tag '{0}' has been renamed to '{1}' with reason: {2}.", }, this); } private string Msg(string key, string userId = null) { return lang.GetMessage(key, this, userId); } #endregion #region Data Model private class RenameEntry { public string TimeUtc { get; set; } public string ExecutorName { get; set; } public string ExecutorId { get; set; } public string OldTag { get; set; } public string NewTag { get; set; } public string Reason { get; set; } } #endregion #region Hooks private void Init() { permission.RegisterPermission(PermUse, this); LoadConfig(); LoadData(); } private void OnServerSave() { SaveData(); } private void Unload() { SaveData(); } private void OnNewSave(string filename) { if (_config == null) { LoadConfig(); } if (_config.WipeLogsOnServerWipe) { _log.Clear(); Interface.Oxide.DataFileSystem.WriteObject(DataFileName, _log); Puts($"[ClanRenameLogs] Detected new save '{filename}'. Log cleared because WipeLogsOnServerWipe = true."); } else { Puts("[ClanRenameLogs] Detected new save, but WipeLogsOnServerWipe = false. Log has been preserved."); } } #endregion #region Commands [ChatCommand("renameclan")] private void CmdRenameClan(BasePlayer player, string command, string[] args) { if (player == null) return; if (!HasPermission(player)) { SendMessage(player, Msg("Error.NoPermission", player.UserIDString)); return; } if (Clans == null) { SendMessage(player, Msg("Error.NoClansPlugin", player.UserIDString)); return; } if (args.Length < 3) { SendMessage(player, Msg("Usage.Chat", player.UserIDString)); SendMessage(player, Msg("Usage.ChatExample", player.UserIDString)); return; } string oldTag = args[0]; string newTag = args[1]; string reason = string.Join(" ", args.Skip(2)); if (string.IsNullOrEmpty(oldTag) || string.IsNullOrEmpty(newTag)) { SendMessage(player, Msg("Error.EmptyTags", player.UserIDString)); return; } bool apiSupported = TryCheckClanExists(oldTag, out bool oldExists); if (apiSupported) { if (!oldExists) { SendMessage(player, string.Format(Msg("Error.ClanNotFound", player.UserIDString), oldTag)); return; } if (TryCheckClanExists(newTag, out bool newExists) && newExists) { SendMessage(player, string.Format(Msg("Warn.NewTagExists", player.UserIDString), newTag)); } } else { SendMessage(player, Msg("Warn.CantVerifyClan", player.UserIDString)); } string cmd = $"clans rename {oldTag} {newTag}"; rust.RunServerCommand(cmd); bool verified = false; if (apiSupported) { bool oldStillExists; bool newExists; bool oldCheck = TryCheckClanExists(oldTag, out oldStillExists); bool newCheck = TryCheckClanExists(newTag, out newExists); if (oldCheck && newCheck) { verified = !oldStillExists && newExists; } } if (!verified && apiSupported) { SendMessage(player, Msg("Warn.RenameFailed", player.UserIDString)); Puts($"[ClanRenameLogs] Verification failed for rename '{{oldTag}}' -> '{{newTag}}' initiated by {player.displayName} ({player.UserIDString})."); return; } LogRename(player.displayName, player.UserIDString, oldTag, newTag, reason); SendDiscordLog(player.displayName, player.UserIDString, oldTag, newTag, reason); SendMessage(player, string.Format(Msg("Info.LoggedRename", player.UserIDString), oldTag, newTag, reason)); } [ConsoleCommand("renameclan")] private void CmdRenameClanConsole(ConsoleSystem.Arg arg) { if (!arg.IsAdmin) { arg.ReplyWith(Msg("Error.NoPermission")); return; } if (Clans == null) { arg.ReplyWith(Msg("Error.NoClansPlugin")); return; } if (arg.Args == null || arg.Args.Length < 3) { arg.ReplyWith(Msg("Usage.Console")); return; } string oldTag = arg.Args[0].ToString().Trim(); string newTag = arg.Args[1].ToString().Trim(); string reason = string.Join(" ", arg.Args.Skip(2).Select(value => value.ToString())); string executorName = "SERVER CONSOLE"; string executorId = "0"; bool apiSupported = TryCheckClanExists(oldTag, out bool oldExists); if (apiSupported) { if (!oldExists) { arg.ReplyWith(string.Format(Msg("Error.ClanNotFound"), oldTag)); return; } if (TryCheckClanExists(newTag, out bool newExists) && newExists) { arg.ReplyWith(string.Format(Msg("Warn.NewTagExists"), newTag)); } } else { arg.ReplyWith(Msg("Warn.CantVerifyClan")); } string cmd = $"clans rename {oldTag} {newTag}"; rust.RunServerCommand(cmd); bool verified = false; if (apiSupported) { bool oldStillExists; bool newExists; bool oldCheck = TryCheckClanExists(oldTag, out oldStillExists); bool newCheck = TryCheckClanExists(newTag, out newExists); if (oldCheck && newCheck) { verified = !oldStillExists && newExists; } } if (!verified && apiSupported) { arg.ReplyWith(Msg("Warn.RenameFailed")); Puts($"[ClanRenameLogs] Verification failed for rename '{{oldTag}}' -> '{{newTag}}' initiated by {executorName} ({executorId})."); return; } LogRename(executorName, executorId, oldTag, newTag, reason); SendDiscordLog(executorName, executorId, oldTag, newTag, reason); arg.ReplyWith(string.Format(Msg("Info.LoggedRenameConsole"), oldTag, newTag, reason)); } #endregion #region Helpers - Clan Checks private bool TryCheckClanExists(string tag, out bool exists) { exists = false; if (Clans == null || string.IsNullOrEmpty(tag)) return false; try { var result = Clans.Call("GetClan", tag); exists = result != null; } catch (Exception e) { PrintWarning($"[ClanRenameLogs] Failed to query Clans plugin for tag '{tag}': {e.Message}"); return false; } return true; } #endregion #region Helpers - General private bool HasPermission(BasePlayer player) { return permission.UserHasPermission(player.UserIDString, PermUse); } private void SendMessage(BasePlayer player, string message) { player.ChatMessage($"[ClanRenameLogs] {message}"); } private void LogRename(string executorName, string executorId, string oldTag, string newTag, string reason) { var entry = new RenameEntry { TimeUtc = DateTime.UtcNow.ToString("o"), // ISO 8601 ExecutorName = executorName, ExecutorId = executorId, OldTag = oldTag, NewTag = newTag, Reason = reason }; _log.Add(entry); SaveData(); } private void LoadData() { try { _log = Interface.Oxide.DataFileSystem.ReadObject>(DataFileName) ?? new List(); } catch { _log = new List(); } } private void SaveData() { Interface.Oxide.DataFileSystem.WriteObject(DataFileName, _log); } #endregion #region Helpers - Discord private void SendDiscordLog(string executorName, string executorId, string oldTag, string newTag, string reason) { var d = _config?.Discord; if (d == null || !d.Enabled || string.IsNullOrEmpty(d.WebhookUrl)) return; string mention = ""; if (d.MentionHere) { mention = "@here "; } else if (!string.IsNullOrEmpty(d.MentionRoleId)) { mention = $"<@&{d.MentionRoleId}> "; } var payload = new { content = string.IsNullOrEmpty(mention) ? null : mention, username = string.IsNullOrEmpty(d.Username) ? "ClanRenamer" : d.Username, avatar_url = string.IsNullOrEmpty(d.AvatarUrl) ? null : d.AvatarUrl, embeds = new[] { new { title = "Clan Rename Logged", description = $"`{oldTag}` → `{newTag}`", color = 15158332, // light red/orange fields = new[] { new { name = "Executor", value = $"{executorName} ({executorId})", inline = false }, new { name = "Reason", value = string.IsNullOrEmpty(reason) ? "No reason provided." : reason, inline = false }, new { name = "Time (UTC)", value = DateTime.UtcNow.ToString("u"), inline = false } } } } }; string json; try { json = JsonConvert.SerializeObject(payload); } catch (Exception e) { PrintWarning($"[ClanRenameLogs] Failed to serialize Discord payload: {e.Message}"); return; } webrequest.Enqueue(d.WebhookUrl, json, (code, response) => { if (code != 204 && code != 200) { PrintWarning($"[ClanRenameLogs] Discord webhook returned code {code}. Response: {response}"); } }, this, Core.Libraries.RequestMethod.POST, new Dictionary { ["Content-Type"] = "application/json" }); } #endregion } }