This patch improves command feedback and message formatting.
Changes in this version:
- Fixed usage message formatting
- Fixed
/prefix in chat and no prefix in console - Fixed player-based localized public messages
- Fixed console messages to remove color tags
- Improved formatting for dynamic values in messages
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace Oxide.Plugins
{
[Info("Better Chat Mute", "LaserHydra", "1.2.4")]
[Description("Simple mute system, made for use with Better Chat")]
internal class BetterChatMute : CovalencePlugin
{
private static Dictionary<string, MuteInfo> _mutes;
private bool _isDataDirty, _globalMute;
#region Hooks
private void Loaded()
{
permission.RegisterPermission("betterchatmute.permanent", this);
LoadData(out _mutes);
SaveData(_mutes);
lang.RegisterMessages(new Dictionary<string, string>
{
["No Permission"] = "You don't have permission to use this command.",
["No Reason"] = "Unknown reason",
["Muted"] = "<color=red>{player}</color> was muted by <color=red>{initiator}</color>: <color=red>{reason}</color>.",
["Muted Time"] = "<color=red>{player}</color> was muted by <color=red>{initiator}</color> for <color=red>{time}</color>: <color=red>{reason}</color>.",
["Unmuted"] = "<color=red>{player}</color> was unmuted by <color=red>{initiator}</color>.",
["Not Muted"] = "<color=red>{player}</color> is currently not muted.",
["Mute Expired"] = "<color=red>{player}</color> is no longer muted.",
["Invalid Time Format"] = "Invalid time format. Example: <color=red>1d2h3m4s</color> = <color=red>1</color> day, <color=red>2</color> hours, <color=red>3</color> min, <color=red>4</color> sec",
["Nobody Muted"] = "There is nobody muted at the moment.",
["Syntax Usage Mute"] = "Usage: {0} <player|steamid> [reason] [time: 1d1h1m1s]",
["Syntax Usage Unmute"] = "Usage: {0} <player|steamid>",
["Player Name Not Found"] = "Could not find player with name <color=red>{name}</color>",
["Player ID Not Found"] = "Could not find player with ID <color=red>{id}</color>",
["Multiple Players Found"] = "Multiple matching players found: \n<color=red>{matches}</color>",
["Time Muted Player Joined"] = "<color=red>{player}</color> is temporarily muted. Remaining time: <color=red>{time}</color>",
["Time Muted Player Chat"] = "You may not chat, you are temporarily muted. Remaining time: <color=red>{time}</color>",
["Muted Player Joined"] = "<color=red>{player}</color> is permanently muted.",
["Muted Player Chat"] = "You may not chat, you are permanently muted.",
["Global Mute Enabled"] = "Global mute was enabled. Nobody can chat while global mute is active.",
["Global Mute Disabled"] = "Global mute was disabled. Everybody can chat again.",
["Global Mute Active"] = "Global mute is active, you may not chat."
}, this);
timer.Repeat(10, 0, () =>
{
List<string> expired = _mutes.Where(m => m.Value.Expired).Select(m => m.Key).ToList();
foreach (string id in expired)
{
var player = players.FindPlayerById(id);
_mutes.Remove(id);
PublicMessage("Mute Expired", new KeyValuePair<string, string>("player", SanitizeName(player?.Name)));
Interface.CallHook("OnBetterChatMuteExpired", player);
if (!_isDataDirty)
_isDataDirty = true;
}
if (_isDataDirty)
{
SaveData(_mutes);
_isDataDirty = false;
}
});
}
#if RUST
private object OnPlayerChat(BasePlayer bplayer, string message, ConVar.Chat.ChatChannel chatChannel)
{
IPlayer player = bplayer.IPlayer;
bool isPublicMessage = chatChannel == ConVar.Chat.ChatChannel.Global;
#else
private object OnUserChat(IPlayer player, string message)
{
bool isPublicMessage = true;
#endif
if (plugins.Exists("BetterChat"))
return null;
return HandleChat(player, isPublicMessage);
}
private void OnBetterChat(Dictionary<string, object> messageData)
{
#if RUST
var chatChannel = (ConVar.Chat.ChatChannel)messageData["ChatChannel"];
bool isPublicMessage = chatChannel == ConVar.Chat.ChatChannel.Global;
#else
bool isPublicMessage = true;
#endif
if (HandleChat((IPlayer)messageData["Player"], isPublicMessage) != null)
{
messageData["CancelOption"] = 2;
}
}
private void OnUserInit(IPlayer player)
{
UpdateMuteStatus(player);
if (MuteInfo.IsMuted(player))
{
if (_mutes[player.Id].Timed)
PublicMessage("Time Muted Player Joined",
new KeyValuePair<string, string>("player", SanitizeName(player.Name)),
new KeyValuePair<string, string>("time", FormatTime(_mutes[player.Id].ExpireDate - DateTime.UtcNow)));
else
PublicMessage("Muted Player Joined", new KeyValuePair<string, string>("player", SanitizeName(player.Name)));
}
}
#endregion
#region Commands
[Command("toggleglobalmute", "bcm.toggleglobalmute"), Permission("betterchatmute.use.global")]
private void CmdGlobalMute(IPlayer player, string cmd, string[] args)
{
_globalMute = !_globalMute;
PublicMessage(_globalMute ? "Global Mute Enabled" : "Global Mute Disabled");
}
[Command("mutelist", "bcm.mutelist"), Permission("betterchatmute.use")]
private void CmdMuteList(IPlayer player, string cmd, string[] args)
{
if (_mutes.Count == 0)
player.Reply(lang.GetMessage("Nobody Muted", this, player.Id));
else
{
player.Reply(string.Join(Environment.NewLine,
_mutes.Select(kvp =>
$"{SanitizeName(players.FindPlayerById(kvp.Key).Name)}: {FormatTime(kvp.Value.ExpireDate - DateTime.UtcNow)}"
).ToArray()
));
}
}
[Command("mute", "bcm.mute"), Permission("betterchatmute.use")]
private void CmdMute(IPlayer player, string cmd, string[] args)
{
if (args.Length == 0)
{
player.Reply(string.Format(lang.GetMessage("Syntax Usage Mute", this, player.Id), GetCommandText(player, "mute")));
return;
}
string reason = string.Empty;
TimeSpan? timeSpan = null;
var target = GetPlayer(args[0], player);
if (target == null)
return;
for (var i = 1; i < args.Length; i++)
{
if (TryParseTimeSpan(args[i], out timeSpan))
{
args[i] = null;
break;
}
}
// No time given; make sure user has permanent muting permission
if (timeSpan == null && !permission.UserHasPermission(player.Id, "betterchatmute.permanent") && player.Id != "server_console")
{
player.Reply(lang.GetMessage("No Permission", this, player.Id));
return;
}
reason = string.Join(" ", args.Skip(1).Where(a => a != null).ToArray());
reason = string.IsNullOrEmpty(reason) ? lang.GetMessage("No Reason", this, player.Id) : reason;
var expireDate = timeSpan == null ? MuteInfo.NonTimedExpireDate : DateTime.UtcNow + (TimeSpan) timeSpan;
_mutes[target.Id] = new MuteInfo(expireDate, reason);
SaveData(_mutes);
if (timeSpan == null)
{
Interface.CallHook("OnBetterChatMuted", target, player, reason);
PublicMessage("Muted",
new KeyValuePair<string, string>("initiator", SanitizeName(player.Name)),
new KeyValuePair<string, string>("player", SanitizeName(target.Name)),
new KeyValuePair<string, string>("reason", reason));
}
else
{
Interface.CallHook("OnBetterChatTimeMuted", target, player, (TimeSpan) timeSpan, reason);
PublicMessage("Muted Time",
new KeyValuePair<string, string>("initiator", SanitizeName(player.Name)),
new KeyValuePair<string, string>("player", SanitizeName(target.Name)),
new KeyValuePair<string, string>("time", FormatTime((TimeSpan) timeSpan)),
new KeyValuePair<string, string>("reason", reason));
}
}
[Command("unmute", "bcm.unmute"), Permission("betterchatmute.use")]
private void CmdUnmute(IPlayer player, string cmd, string[] args)
{
if (args.Length != 1)
{
player.Reply(string.Format(lang.GetMessage("Syntax Usage Unmute", this, player.Id), GetCommandText(player, "unmute")));
return;
}
IPlayer target = GetPlayer(args[0], player);
if (target == null)
return;
if (!MuteInfo.IsMuted(target))
{
player.Reply(lang.GetMessage("Not Muted", this, player.Id).Replace("{player}", SanitizeName(target.Name)));
return;
}
_mutes.Remove(target.Id);
SaveData(_mutes);
Interface.CallHook("OnBetterChatUnmuted", target, player);
PublicMessage("Unmuted",
new KeyValuePair<string, string>("initiator", SanitizeName(player.Name)),
new KeyValuePair<string, string>("player", SanitizeName(target.Name)));
}
#endregion
#region API Methods
private void API_Mute(IPlayer target, IPlayer player, string reason = "", bool callHook = true, bool broadcast = true)
{
_mutes[target.Id] = new MuteInfo(MuteInfo.NonTimedExpireDate, reason);
SaveData(_mutes);
reason = string.IsNullOrEmpty(reason) ? lang.GetMessage("No Reason", this, player.Id) : reason;
if (callHook)
Interface.CallHook("OnBetterChatMuted", target, player, reason);
if (broadcast)
{
PublicMessage("Muted",
new KeyValuePair<string, string>("initiator", SanitizeName(player.Name)),
new KeyValuePair<string, string>("player", SanitizeName(target.Name)),
new KeyValuePair<string, string>("reason", reason));
}
}
private void API_TimeMute(IPlayer target, IPlayer player, TimeSpan timeSpan, string reason = "", bool callHook = true, bool broadcast = true)
{
_mutes[target.Id] = new MuteInfo(DateTime.UtcNow + timeSpan, reason);
SaveData(_mutes);
reason = string.IsNullOrEmpty(reason) ? lang.GetMessage("No Reason", this, player.Id) : reason;
if (callHook)
Interface.CallHook("OnBetterChatTimeMuted", target, player, timeSpan,
string.IsNullOrEmpty(reason) ? lang.GetMessage("No Reason", this, player.Id) : reason);
if (broadcast)
{
PublicMessage("Muted Time",
new KeyValuePair<string, string>("initiator", SanitizeName(player.Name)),
new KeyValuePair<string, string>("player", SanitizeName(target.Name)),
new KeyValuePair<string, string>("time", FormatTime(timeSpan)),
new KeyValuePair<string, string>("reason", reason));
}
}
private bool API_Unmute(IPlayer target, IPlayer player, bool callHook = true, bool broadcast = true)
{
if (!MuteInfo.IsMuted(target))
return false;
_mutes.Remove(target.Id);
SaveData(_mutes);
if (callHook)
Interface.CallHook("OnBetterChatUnmuted", target, player);
if (broadcast)
{
PublicMessage("Unmuted",
new KeyValuePair<string, string>("initiator", SanitizeName(player.Name)),
new KeyValuePair<string, string>("player", SanitizeName(target.Name)));
}
return true;
}
private void API_SetGlobalMuteState(bool state, bool broadcast = true)
{
_globalMute = state;
if (broadcast)
PublicMessage(_globalMute ? "Global Mute Enabled" : "Global Mute Disabled");
}
private bool API_GetGlobalMuteState() => _globalMute;
private bool API_IsMuted(IPlayer player) => _mutes.ContainsKey(player.Id);
private List<string> API_GetMuteList() => _mutes.Keys.ToList();
#endregion
#region Helpers
private string SanitizeName(string name)
{
if (string.IsNullOrEmpty(name))
return name;
return name.Replace("<", "‹").Replace(">", "›");
}
private void PublicMessage(string key, params KeyValuePair<string, string>[] replacements)
{
foreach (var target in players.Connected)
{
var message = lang.GetMessage(key, this, target.Id);
foreach (var replacement in replacements)
message = message.Replace($"{{{replacement.Key}}}", replacement.Value);
target.Message(message);
}
var consoleMessage = lang.GetMessage(key, this);
foreach (var replacement in replacements)
consoleMessage = consoleMessage.Replace($"{{{replacement.Key}}}", replacement.Value);
Puts(StripRichText(consoleMessage));
}
private string StripRichText(string text)
{
if (string.IsNullOrEmpty(text))
return text;
text = Regex.Replace(text, @"<color=.*?>", string.Empty, RegexOptions.IgnoreCase);
text = Regex.Replace(text, @"</color>", string.Empty, RegexOptions.IgnoreCase);
return text;
}
private string GetCommandText(IPlayer player, string command)
{
bool isConsole = player == null || player.Id == "server_console";
return $"{(isConsole ? "" : "/")}{command}";
}
private object HandleChat(IPlayer player, bool isPublicChat)
{
if (!isPublicChat)
return null;
UpdateMuteStatus(player);
var result = Interface.CallHook("OnBetterChatMuteHandle", player, MuteInfo.IsMuted(player) ? JObject.FromObject(_mutes[player.Id]) : null);
if (result != null)
return result;
if (MuteInfo.IsMuted(player))
{
if (_mutes[player.Id].Timed)
{
player.Reply(lang.GetMessage("Time Muted Player Chat", this, player.Id)
.Replace("{time}", FormatTime(_mutes[player.Id].ExpireDate - DateTime.UtcNow))
);
}
else
{
player.Reply(lang.GetMessage("Muted Player Chat", this, player.Id));
}
return true;
}
if (_globalMute && !permission.UserHasPermission(player.Id, "betterchatmute.use.global"))
{
player.Reply(lang.GetMessage("Global Mute Active", this, player.Id));
return true;
}
return null;
}
private void UpdateMuteStatus(IPlayer player)
{
if (MuteInfo.IsMuted(player) && _mutes[player.Id].Expired)
{
_mutes.Remove(player.Id);
SaveData(_mutes);
PublicMessage("Mute Expired", new KeyValuePair<string, string>("player", SanitizeName(players.FindPlayerById(player.Id)?.Name)));
Interface.CallHook("OnBetterChatMuteExpired", player);
}
}
private IPlayer GetPlayer(string nameOrId, IPlayer requestor)
{
if (nameOrId.IsSteamId())
{
IPlayer player = players.All.ToList().Find(p => p.Id == nameOrId);
if (player == null)
requestor.Reply(lang.GetMessage("Player ID Not Found", this, requestor.Id).Replace("{id}", nameOrId));
return player;
}
List<IPlayer> foundPlayers = new List<IPlayer>();
foreach (var player in players.Connected)
{
if (string.Equals(player.Name, nameOrId, StringComparison.CurrentCultureIgnoreCase))
return player;
if (player.Name.ToLower().Contains(nameOrId.ToLower()))
foundPlayers.Add(player);
}
switch (foundPlayers.Count)
{
case 0:
requestor.Reply(lang.GetMessage("Player Name Not Found", this, requestor.Id).Replace("{name}", nameOrId));
break;
case 1:
return foundPlayers[0];
default:
var names = (from current in foundPlayers select SanitizeName(current.Name)).ToArray();
requestor.Reply(lang.GetMessage("Multiple Players Found", this, requestor.Id).Replace("{matches}", string.Join(", ", names)));
break;
}
return null;
}
#region DateTime Helper
private static string FormatTime(TimeSpan time)
{
var values = new List<string>();
if (time.Days != 0)
values.Add($"{time.Days} day(s)");
if (time.Hours != 0)
values.Add($"{time.Hours} hour(s)");
if (time.Minutes != 0)
values.Add($"{time.Minutes} minute(s)");
if (time.Seconds != 0)
values.Add($"{time.Seconds} second(s)");
return values.ToSentence();
}
private static bool TryParseTimeSpan(string source, out TimeSpan? timeSpan)
{
int seconds = 0, minutes = 0, hours = 0, days = 0;
Match s = new Regex(@"(\d+?)s", RegexOptions.IgnoreCase).Match(source);
Match m = new Regex(@"(\d+?)m", RegexOptions.IgnoreCase).Match(source);
Match h = new Regex(@"(\d+?)h", RegexOptions.IgnoreCase).Match(source);
Match d = new Regex(@"(\d+?)d", RegexOptions.IgnoreCase).Match(source);
if (s.Success)
seconds = Convert.ToInt32(s.Groups[1].ToString());
if (m.Success)
minutes = Convert.ToInt32(m.Groups[1].ToString());
if (h.Success)
hours = Convert.ToInt32(h.Groups[1].ToString());
if (d.Success)
days = Convert.ToInt32(d.Groups[1].ToString());
source = source.Replace(seconds + "s", string.Empty);
source = source.Replace(minutes + "m", string.Empty);
source = source.Replace(hours + "h", string.Empty);
source = source.Replace(days + "d", string.Empty);
if (!string.IsNullOrEmpty(source) || !(s.Success || m.Success || h.Success || d.Success))
{
timeSpan = null;
return false;
}
timeSpan = new TimeSpan(days, hours, minutes, seconds);
return true;
}
#endregion
#region Data & Config Helper
private string DataFileName => Title.Replace(" ", "");
private void LoadData<T>(out T data, string filename = null) => data = Interface.Oxide.DataFileSystem.ReadObject<T>(filename ?? DataFileName);
private void SaveData<T>(T data, string filename = null) => Interface.Oxide.DataFileSystem.WriteObject(filename ?? DataFileName, data);
#endregion
#endregion
#region Classes
public class MuteInfo
{
public DateTime ExpireDate = DateTime.MinValue;
[JsonIgnore]
public bool Timed => ExpireDate != DateTime.MinValue;
[JsonIgnore]
public bool Expired => Timed && ExpireDate < DateTime.UtcNow;
public string Reason { get; set; }
public static bool IsMuted(IPlayer player) => _mutes.ContainsKey(player.Id);
public static readonly DateTime NonTimedExpireDate = DateTime.MinValue;
public MuteInfo()
{
}
public MuteInfo(DateTime expireDate, string reason)
{
ExpireDate = expireDate;
Reason = reason;
}
}
#endregion
}
}