C#:
// 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<RenameEntry> _log = new List<RenameEntry>();
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<Configuration>();
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<string, string>
{
["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($"<color=#ffa500>[ClanRenameLogs]</color> {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<List<RenameEntry>>(DataFileName) ?? new List<RenameEntry>();
}
catch
{
_log = new List<RenameEntry>();
}
}
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<string, string>
{
["Content-Type"] = "application/json"
});
}
#endregion
}
}