C#:
using System;
using System.Collections.Generic;
using System.IO;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("PrivateMessages", "SeesAll", "1.3.2")]
[Description("Allows users to send private messages to each other")]
class PrivateMessages : CovalencePlugin
{
private const string AllowPermission = "privatemessages.allow";
private const int MaxHistoryEntries = 5;
private const int UiPlayersPerPage = 10;
private const string AuditLogDirectoryName = "PrivateMessages";
private const string UiPickerPanel = "PrivateMessages.UI.Picker";
private const string UiComposePanel = "PrivateMessages.UI.Compose";
private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1);
private readonly Dictionary<string, string> pmHistory = new Dictionary<string, string>();
private readonly Dictionary<string, double> cooldowns = new Dictionary<string, double>();
private readonly Dictionary<string, ConversationHistory> conversationHistory = new Dictionary<string, ConversationHistory>();
private readonly Dictionary<string, string> selectedPmTargets = new Dictionary<string, string>();
[PluginReference] private Plugin Ignore, UFilter, BetterChatMute;
private ConfigData configData;
private string auditLogDirectoryPath;
private double nextHistoryPruneAt;
private class ConversationHistory
{
public readonly List<string> Messages = new List<string>();
public double LastUpdated;
}
#region Localization
private string Lang(string key, string id = null, params object[] args)
{
return string.Format(lang.GetMessage(key, this, id), args);
}
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
{"PMTo", "[#00FFFF]PM to {0}[/#]: {1}"},
{"PMFrom", "[#00FFFF]PM from {0}[/#]: {1}"},
{"PlayerNotOnline", "{0} is not online."},
{"NotOnlineAnymore", "The last person you was talking to is not online anymore."},
{"NotMessaged", "You haven't messaged anyone or they haven't messaged you."},
{"IgnoreYou", "[#FF0000]{0} is ignoring you and cant receive your PMs[/#]"},
{"SelfPM", "You can not send messages to yourself."},
{"SyntaxR", "Incorrect Syntax use: /r <msg>"},
{"HistorySyntax", "Incorrect Syntax use: /pmhistory <name>"},
{"SyntaxPM", "Incorrect Syntax use: /{0} <name> <msg>"},
{"PmUiUsage", "Open /{0} to choose a player and send a private message. You can also still use /{0} <name> <message>."},
{"PmUiSelected", "Selected [#00FFFF]{0}[/#] for private messages. Now type [#00FFFF]/{1} <message>[/#]."},
{"PmUiSelectedOffline", "Your selected PM target is no longer online. Open /{0} again to pick another player."},
{"PmUiNoPlayers", "No other online players are available to message."},
{"PmUiTitle", "Private Message"},
{"PmUiClose", "Close"},
{"PmUiBack", "Back"},
{"PmUiPrev", "< Prev"},
{"PmUiNext", "Next >"},
{"PmUiInstructions", "Click a player to open the PM composer"},
{"PmUiComposeTitle", "Private Message Composer"},
{"PmUiComposeTo", "To: {0}"},
{"PmUiComposeHint", "Type your message below and press Enter to send."},
{"PmUiComposeOpened", "Opened the PM composer for [#00FFFF]{0}[/#]."},
{"PmUiEmptyMessage", "Type a message before sending."},
{"NotAllowedToChat", "You are not allowed to chat here"},
{"History", "Your History:\n{0}"},
{"CooldownMessage", "You will be able to send a private message in {0} seconds"},
{"NoHistory", "There is not any saved pm history with this player."},
{"CannotFindUser", "Cannot find this user"},
{"MultiplePlayersFound", "Multiple players matched '{0}': {1}. Please be more specific."},
{"CommandDisabled", "This command has been disabled"},
{"IsMuted", "You are currently muted & cannot send private messages"},
{"TargetMuted", "This person is muted & cannot receive your private message"},
{"NoPermission", "You don't have the correct permissions to run this command"},
{"HistoryPM", "[#00FFFF]{0}[/#]: {1}"},
{"Logging", "[PM]{0}->{1}:{2}"}
}, this);
}
#endregion
#region Hooks
private void Init()
{
LoadVariables();
AddCovalenceCommand(string.IsNullOrEmpty(configData.PmCommand) ? "pm" : configData.PmCommand, nameof(cmdPm));
AddCovalenceCommand("r", nameof(cmdPmReply));
AddCovalenceCommand("pmhistory", nameof(cmdPmHistory));
permission.RegisterPermission(AllowPermission, this);
auditLogDirectoryPath = Path.Combine("oxide", "logs", AuditLogDirectoryName);
Directory.CreateDirectory(auditLogDirectoryPath);
}
private void OnUserDisconnected(IPlayer player)
{
if (player == null)
{
return;
}
pmHistory.Remove(player.Id);
cooldowns.Remove(player.Id);
selectedPmTargets.Remove(player.Id);
RemoveStaleReplyTargets(player.Id);
RemovePlayerConversationHistory(player.Id);
RemovePlayerSelections(player.Id);
DestroyPmUi(player);
}
private void Unload()
{
foreach (var player in players.Connected)
{
DestroyPmUi(player);
}
}
private void OnNewSave(string filename)
{
if (!configData.ClearAuditLogsOnNewSave)
{
return;
}
ClearAuditLogs();
}
#endregion
#region Commands
private void cmdPm(IPlayer player, string command, string[] args)
{
if (!HasPmPermission(player))
{
return;
}
if (args == null || args.Length == 0)
{
OpenPmPickerUi(player, 0);
return;
}
string selectedTargetId;
if (selectedPmTargets.TryGetValue(player.Id, out selectedTargetId))
{
string selectedTargetError;
var selectedTarget = FindConnectedPlayer(selectedTargetId, out selectedTargetError);
if (selectedTarget == null)
{
selectedPmTargets.Remove(player.Id);
player.Reply(Lang("PmUiSelectedOffline", player.Id, configData.PmCommand));
OpenPmPickerUi(player, 0);
return;
}
HandlePm(player, selectedTarget, BuildMessage(args, 0));
return;
}
if (args.Length < 2)
{
player.Reply(Lang("SyntaxPM", player.Id, configData.PmCommand));
player.Reply(Lang("PmUiUsage", player.Id, configData.PmCommand));
return;
}
string lookupError;
var target = FindConnectedPlayer(args[0], out lookupError);
if (target == null)
{
player.Reply(string.IsNullOrEmpty(lookupError) ? Lang("PlayerNotOnline", player.Id, args[0]) : lookupError);
return;
}
if (target.Id == player.Id)
{
player.Reply(Lang("SelfPM", player.Id));
return;
}
HandlePm(player, target, BuildMessage(args, 1));
}
private void cmdPmReply(IPlayer player, string command, string[] args)
{
if (!HasPmPermission(player))
{
return;
}
if (args == null || args.Length == 0)
{
player.Reply(Lang("SyntaxR", player.Id));
return;
}
string targetId;
if (!pmHistory.TryGetValue(player.Id, out targetId))
{
player.Reply(Lang("NotMessaged", player.Id));
return;
}
string lookupError;
var target = FindConnectedPlayer(targetId, out lookupError);
if (target == null)
{
pmHistory.Remove(player.Id);
player.Reply(Lang("NotOnlineAnymore", player.Id));
return;
}
HandlePm(player, target, BuildMessage(args, 0));
}
private void cmdPmHistory(IPlayer player, string command, string[] args)
{
if (!configData.EnableHistory)
{
player.Reply(Lang("CommandDisabled", player.Id));
return;
}
PruneInactiveConversationHistoryIfNeeded();
if (args == null || args.Length != 1)
{
player.Reply(Lang("HistorySyntax", player.Id));
return;
}
var target = covalence.Players.FindPlayer(args[0]);
if (target == null)
{
player.Reply(Lang("CannotFindUser", player.Id));
return;
}
var history = GetConversationHistory(player.Id, target.Id);
if (history == null || history.Messages.Count == 0)
{
player.Reply(Lang("NoHistory", player.Id));
return;
}
player.Reply(Lang("History", player.Id, string.Join(Environment.NewLine, history.Messages)));
}
[ConsoleCommand("privatemessages.ui.close")]
private void ConsolePmUiClose(ConsoleSystem.Arg arg)
{
var basePlayer = arg?.Player();
if (basePlayer == null)
{
return;
}
DestroyPmUi(basePlayer.IPlayer);
}
[ConsoleCommand("privatemessages.ui.page")]
private void ConsolePmUiPage(ConsoleSystem.Arg arg)
{
var basePlayer = arg?.Player();
if (basePlayer == null)
{
return;
}
var player = basePlayer.IPlayer;
if (!HasPmPermission(player))
{
DestroyPmUi(player);
return;
}
int page;
if (!HasConsoleArgs(arg) || !int.TryParse(GetConsoleArg(arg, 0), out page))
{
page = 0;
}
OpenPmPickerUi(player, page);
}
[ConsoleCommand("privatemessages.ui.select")]
private void ConsolePmUiSelect(ConsoleSystem.Arg arg)
{
var basePlayer = arg?.Player();
if (basePlayer == null)
{
return;
}
var player = basePlayer.IPlayer;
if (!HasPmPermission(player))
{
DestroyPmUi(player);
return;
}
if (!HasConsoleArgs(arg))
{
OpenPmPickerUi(player, 0);
return;
}
var targetArg = GetConsoleArg(arg, 0);
string lookupError;
var target = FindConnectedPlayer(targetArg, out lookupError);
if (target == null)
{
player.Reply(string.IsNullOrEmpty(lookupError) ? Lang("PlayerNotOnline", player.Id, targetArg) : lookupError);
OpenPmPickerUi(player, 0);
return;
}
if (target.Id == player.Id)
{
player.Reply(Lang("SelfPM", player.Id));
OpenPmPickerUi(player, 0);
return;
}
selectedPmTargets[player.Id] = target.Id;
OpenPmComposerUi(player, target);
player.Reply(Lang("PmUiComposeOpened", player.Id, target.Name));
}
[ConsoleCommand("privatemessages.ui.back")]
private void ConsolePmUiBack(ConsoleSystem.Arg arg)
{
var basePlayer = arg?.Player();
if (basePlayer == null)
{
return;
}
var player = basePlayer.IPlayer;
if (!HasPmPermission(player))
{
DestroyPmUi(player);
return;
}
OpenPmPickerUi(player, 0);
}
[ConsoleCommand("privatemessages.ui.send")]
private void ConsolePmUiSend(ConsoleSystem.Arg arg)
{
var basePlayer = arg?.Player();
if (basePlayer == null)
{
return;
}
var player = basePlayer.IPlayer;
if (!HasPmPermission(player))
{
DestroyPmUi(player);
return;
}
if (!HasConsoleArgs(arg))
{
OpenPmPickerUi(player, 0);
return;
}
var targetArg = GetConsoleArg(arg, 0);
string lookupError;
var target = FindConnectedPlayer(targetArg, out lookupError);
if (target == null)
{
selectedPmTargets.Remove(player.Id);
player.Reply(string.IsNullOrEmpty(lookupError) ? Lang("PlayerNotOnline", player.Id, targetArg) : lookupError);
OpenPmPickerUi(player, 0);
return;
}
var message = BuildConsoleArgMessage(arg, 1);
if (string.IsNullOrWhiteSpace(message))
{
player.Reply(Lang("PmUiEmptyMessage", player.Id));
OpenPmComposerUi(player, target);
return;
}
DestroyPmUi(player);
HandlePm(player, target, message);
}
#endregion
#region PM Processing
private void HandlePm(IPlayer sender, IPlayer target, string rawMessage)
{
if (!(bool)(Interface.Oxide.CallHook("CanChat", sender) ?? true))
{
sender.Reply(Lang("NotAllowedToChat", sender.Id));
return;
}
if (configData.UseBetterChatMute && BetterChatMute != null && CheckMuteStatus(sender, target))
{
return;
}
if (IsOnCooldown(sender))
{
return;
}
if (IsIgnored(sender, target))
{
return;
}
var message = ProcessMessage(rawMessage);
if (string.IsNullOrWhiteSpace(message))
{
return;
}
if (Interface.Oxide.CallHook("OnPMProcessed", sender, target, message) != null)
{
return;
}
AddPmHistory(sender.Id, target.Id);
selectedPmTargets[sender.Id] = target.Id;
sender.Reply(Lang("PMTo", sender.Id, target.Name, message));
target.Reply(Lang("PMFrom", target.Id, sender.Name, message));
AddHistoryAndLogging(sender, target, message);
}
private bool HasPmPermission(IPlayer player)
{
if (!configData.UsePermission)
{
return true;
}
if (player.HasPermission(AllowPermission))
{
return true;
}
player.Reply(Lang("NoPermission", player.Id));
return false;
}
private void AddHistoryAndLogging(IPlayer initiator, IPlayer target, string message)
{
PruneInactiveConversationHistoryIfNeeded();
if (configData.EnableHistory)
{
AddToHistory(initiator.Id, target.Id, Lang("HistoryPM", null, initiator.Name, message));
}
if (configData.EnableConsoleLogging)
{
Puts(Lang("Logging", null, initiator.Name, target.Name, message));
}
if (configData.EnableFileAuditLogging)
{
WriteAuditLog(initiator, target, message);
}
}
private void AddPmHistory(string initiatorId, string targetId)
{
pmHistory[initiatorId] = targetId;
pmHistory[targetId] = initiatorId;
}
private bool CheckMuteStatus(IPlayer player, IPlayer target)
{
var playerMuted = BetterChatMute.CallHook("API_IsMuted", player);
if (playerMuted is bool && (bool)playerMuted)
{
player.Reply(Lang("IsMuted", player.Id));
return true;
}
var targetMuted = BetterChatMute.CallHook("API_IsMuted", target);
if (targetMuted is bool && (bool)targetMuted)
{
player.Reply(Lang("TargetMuted", player.Id));
return true;
}
return false;
}
private bool IsIgnored(IPlayer sender, IPlayer target)
{
if (!configData.UseIgnore)
{
return false;
}
var ignored = Ignore?.CallHook("HasIgnored", target.Id, sender.Id);
if (!(ignored is bool) || !(bool)ignored)
{
return false;
}
sender.Reply(Lang("IgnoreYou", sender.Id, target.Name));
return true;
}
private bool IsOnCooldown(IPlayer player)
{
if (!configData.UseCooldown)
{
return false;
}
var now = GetTimeStamp();
double expiresAt;
if (cooldowns.TryGetValue(player.Id, out expiresAt))
{
if (expiresAt > now)
{
player.Reply(Lang("CooldownMessage", player.Id, Math.Round(expiresAt - now, 2)));
return true;
}
cooldowns.Remove(player.Id);
}
cooldowns[player.Id] = now + configData.CooldownTime;
return false;
}
private string ProcessMessage(string message)
{
if (configData.UseUFilter && UFilter != null)
{
var filtered = UFilter.Call("ProcessText", message);
if (filtered != null)
{
message = filtered.ToString();
}
}
return RemoveRichText(message);
}
private static string BuildMessage(string[] args, int startIndex)
{
if (args == null || args.Length <= startIndex)
{
return string.Empty;
}
return string.Join(" ", args, startIndex, args.Length - startIndex);
}
private static bool HasConsoleArgs(ConsoleSystem.Arg arg)
{
return arg?.Args != null && arg.Args.Length > 0;
}
private static string GetConsoleArg(ConsoleSystem.Arg arg, int index)
{
if (arg?.Args == null || index < 0 || index >= arg.Args.Length)
{
return string.Empty;
}
return arg.GetString(index) ?? string.Empty;
}
private static string BuildConsoleArgMessage(ConsoleSystem.Arg arg, int startIndex)
{
if (arg?.Args == null || arg.Args.Length <= startIndex)
{
return string.Empty;
}
var parts = new string[arg.Args.Length - startIndex];
for (var i = startIndex; i < arg.Args.Length; i++)
{
parts[i - startIndex] = arg.GetString(i) ?? string.Empty;
}
return string.Join(" ", parts);
}
#endregion
#region Player Lookup
private IPlayer FindConnectedPlayer(string nameOrIdOrIp, out string errorMessage)
{
errorMessage = null;
if (string.IsNullOrEmpty(nameOrIdOrIp))
{
return null;
}
IPlayer partialMatch = null;
var multipleMatches = new List<string>();
var search = nameOrIdOrIp.Trim();
foreach (var activePlayer in covalence.Players.Connected)
{
if (activePlayer == null)
{
continue;
}
if (string.Equals(activePlayer.Id, search, StringComparison.Ordinal) ||
string.Equals(activePlayer.Address, search, StringComparison.Ordinal) ||
string.Equals(activePlayer.Name, search, StringComparison.OrdinalIgnoreCase))
{
return activePlayer;
}
if (string.IsNullOrEmpty(activePlayer.Name) ||
activePlayer.Name.IndexOf(search, StringComparison.OrdinalIgnoreCase) < 0)
{
continue;
}
if (partialMatch == null)
{
partialMatch = activePlayer;
multipleMatches.Add(activePlayer.Name);
continue;
}
multipleMatches.Add(activePlayer.Name);
}
if (multipleMatches.Count > 1)
{
errorMessage = Lang("MultiplePlayersFound", null, search, string.Join(", ", multipleMatches));
return null;
}
return partialMatch;
}
private List<IPlayer> GetPmPickerCandidates(IPlayer requester)
{
var availablePlayers = new List<IPlayer>();
foreach (var activePlayer in covalence.Players.Connected)
{
if (activePlayer == null || activePlayer.Id == requester.Id || string.IsNullOrEmpty(activePlayer.Name))
{
continue;
}
availablePlayers.Add(activePlayer);
}
availablePlayers.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
return availablePlayers;
}
#endregion
#region UI
private void OpenPmPickerUi(IPlayer player, int page)
{
var basePlayer = player.Object as BasePlayer;
if (basePlayer == null)
{
player.Reply(Lang("PmUiUsage", player.Id, configData.PmCommand));
return;
}
var availablePlayers = GetPmPickerCandidates(player);
if (availablePlayers.Count == 0)
{
DestroyPmUi(player);
player.Reply(Lang("PmUiNoPlayers", player.Id));
return;
}
var maxPage = Math.Max(0, (availablePlayers.Count - 1) / UiPlayersPerPage);
if (page < 0)
{
page = 0;
}
else if (page > maxPage)
{
page = maxPage;
}
DestroyPmUi(player);
var elements = new CuiElementContainer();
elements.Add(new CuiPanel
{
Image = { Color = "0.10 0.10 0.10 0.96" },
RectTransform = { AnchorMin = "0.34 0.24", AnchorMax = "0.66 0.76" },
CursorEnabled = true
}, "Overlay", UiPickerPanel);
elements.Add(new CuiLabel
{
Text =
{
Text = Lang("PmUiTitle", player.Id),
FontSize = 18,
Align = TextAnchor.MiddleCenter,
Color = "1 1 1 1"
},
RectTransform = { AnchorMin = "0.05 0.90", AnchorMax = "0.95 0.98" }
}, UiPickerPanel);
elements.Add(new CuiLabel
{
Text =
{
Text = Lang("PmUiInstructions", player.Id, configData.PmCommand),
FontSize = 12,
Align = TextAnchor.MiddleCenter,
Color = "0.85 0.85 0.85 1"
},
RectTransform = { AnchorMin = "0.05 0.83", AnchorMax = "0.95 0.90" }
}, UiPickerPanel);
elements.Add(new CuiButton
{
Button = { Color = "0.60 0.20 0.20 1.0", Command = "privatemessages.ui.close" },
RectTransform = { AnchorMin = "0.74 0.05", AnchorMax = "0.95 0.12" },
Text = { Text = Lang("PmUiClose", player.Id), FontSize = 13, Align = TextAnchor.MiddleCenter }
}, UiPickerPanel);
if (page > 0)
{
elements.Add(new CuiButton
{
Button = { Color = "0.20 0.35 0.50 1.0", Command = string.Format("privatemessages.ui.page {0}", page - 1) },
RectTransform = { AnchorMin = "0.05 0.05", AnchorMax = "0.22 0.12" },
Text = { Text = Lang("PmUiPrev", player.Id), FontSize = 13, Align = TextAnchor.MiddleCenter }
}, UiPickerPanel);
}
if (page < maxPage)
{
elements.Add(new CuiButton
{
Button = { Color = "0.20 0.35 0.50 1.0", Command = string.Format("privatemessages.ui.page {0}", page + 1) },
RectTransform = { AnchorMin = "0.24 0.05", AnchorMax = "0.41 0.12" },
Text = { Text = Lang("PmUiNext", player.Id), FontSize = 13, Align = TextAnchor.MiddleCenter }
}, UiPickerPanel);
}
elements.Add(new CuiLabel
{
Text =
{
Text = string.Format("{0}/{1}", page + 1, maxPage + 1),
FontSize = 12,
Align = TextAnchor.MiddleCenter,
Color = "0.85 0.85 0.85 1"
},
RectTransform = { AnchorMin = "0.44 0.05", AnchorMax = "0.56 0.12" }
}, UiPickerPanel);
var startIndex = page * UiPlayersPerPage;
var endIndex = Math.Min(startIndex + UiPlayersPerPage, availablePlayers.Count);
var rowHeight = 0.068f;
var top = 0.78f;
for (var index = startIndex; index < endIndex; index++)
{
var row = index - startIndex;
var rowTop = top - (row * rowHeight);
var rowBottom = rowTop - 0.052f;
var target = availablePlayers[index];
var isSelected = selectedPmTargets.ContainsKey(player.Id) && selectedPmTargets[player.Id] == target.Id;
var buttonColor = isSelected ? "0.18 0.45 0.18 1.0" : "0.18 0.18 0.18 1.0";
elements.Add(new CuiButton
{
Button = { Color = buttonColor, Command = string.Format("privatemessages.ui.select {0}", target.Id) },
RectTransform = { AnchorMin = string.Format("0.07 {0:0.000}", rowBottom), AnchorMax = string.Format("0.93 {0:0.000}", rowTop) },
Text = { Text = SanitizeUiText(target.Name), FontSize = 13, Align = TextAnchor.MiddleLeft, Color = "1 1 1 1" }
}, UiPickerPanel);
}
CuiHelper.AddUi(basePlayer, elements);
}
private void OpenPmComposerUi(IPlayer player, IPlayer target)
{
var basePlayer = player.Object as BasePlayer;
if (basePlayer == null || target == null)
{
OpenPmPickerUi(player, 0);
return;
}
DestroyPmUi(player);
var elements = new CuiElementContainer();
elements.Add(new CuiPanel
{
Image = { Color = "0.10 0.10 0.10 0.96" },
RectTransform = { AnchorMin = "0.30 0.30", AnchorMax = "0.70 0.68" },
CursorEnabled = true
}, "Overlay", UiComposePanel);
elements.Add(new CuiElement
{
Parent = UiComposePanel,
Components =
{
new CuiNeedsCursorComponent(),
new CuiNeedsKeyboardComponent()
}
});
elements.Add(new CuiLabel
{
Text =
{
Text = Lang("PmUiComposeTitle", player.Id),
FontSize = 18,
Align = TextAnchor.MiddleCenter,
Color = "1 1 1 1"
},
RectTransform = { AnchorMin = "0.05 0.84", AnchorMax = "0.95 0.95" }
}, UiComposePanel);
elements.Add(new CuiLabel
{
Text =
{
Text = Lang("PmUiComposeTo", player.Id, SanitizeUiText(target.Name)),
FontSize = 14,
Align = TextAnchor.MiddleCenter,
Color = "0.90 0.90 0.90 1"
},
RectTransform = { AnchorMin = "0.05 0.72", AnchorMax = "0.95 0.82" }
}, UiComposePanel);
elements.Add(new CuiLabel
{
Text =
{
Text = Lang("PmUiComposeHint", player.Id),
FontSize = 12,
Align = TextAnchor.MiddleCenter,
Color = "0.85 0.85 0.85 1"
},
RectTransform = { AnchorMin = "0.05 0.60", AnchorMax = "0.95 0.70" }
}, UiComposePanel);
elements.Add(new CuiPanel
{
Image = { Color = "0.16 0.16 0.16 1.0" },
RectTransform = { AnchorMin = "0.07 0.35", AnchorMax = "0.93 0.54" }
}, UiComposePanel, UiComposePanel + ".InputBg");
elements.Add(new CuiElement
{
Name = UiComposePanel + ".Input",
Parent = UiComposePanel + ".InputBg",
Components =
{
new CuiInputFieldComponent
{
Align = TextAnchor.MiddleLeft,
CharsLimit = 180,
Command = string.Format("privatemessages.ui.send {0}", target.Id),
FontSize = 14,
IsPassword = false,
Text = string.Empty,
Color = "1 1 1 1"
},
new CuiRectTransformComponent { AnchorMin = "0.02 0.10", AnchorMax = "0.98 0.90" }
}
});
elements.Add(new CuiButton
{
Button = { Color = "0.25 0.35 0.50 1.0", Command = "privatemessages.ui.back" },
RectTransform = { AnchorMin = "0.07 0.10", AnchorMax = "0.26 0.20" },
Text = { Text = Lang("PmUiBack", player.Id), FontSize = 13, Align = TextAnchor.MiddleCenter }
}, UiComposePanel);
elements.Add(new CuiButton
{
Button = { Color = "0.60 0.20 0.20 1.0", Command = "privatemessages.ui.close" },
RectTransform = { AnchorMin = "0.74 0.10", AnchorMax = "0.93 0.20" },
Text = { Text = Lang("PmUiClose", player.Id), FontSize = 13, Align = TextAnchor.MiddleCenter }
}, UiComposePanel);
CuiHelper.AddUi(basePlayer, elements);
}
private void DestroyPmUi(IPlayer player)
{
var basePlayer = player?.Object as BasePlayer;
if (basePlayer == null)
{
return;
}
CuiHelper.DestroyUi(basePlayer, UiPickerPanel);
CuiHelper.DestroyUi(basePlayer, UiComposePanel);
}
private static string SanitizeUiText(string text)
{
return string.IsNullOrEmpty(text) ? string.Empty : text.Replace("\n", " ").Replace("\r", " ");
}
#endregion
#region History
private void AddToHistory(string senderId, string targetId, string message)
{
var history = GetOrCreateConversationHistory(senderId, targetId);
history.LastUpdated = GetTimeStamp();
history.Messages.Add(message);
if (history.Messages.Count > MaxHistoryEntries)
{
history.Messages.RemoveAt(0);
}
}
private ConversationHistory GetConversationHistory(string senderId, string targetId)
{
ConversationHistory history;
return conversationHistory.TryGetValue(GetConversationKey(senderId, targetId), out history) ? history : null;
}
private ConversationHistory GetOrCreateConversationHistory(string senderId, string targetId)
{
var key = GetConversationKey(senderId, targetId);
ConversationHistory history;
if (!conversationHistory.TryGetValue(key, out history))
{
history = new ConversationHistory
{
LastUpdated = GetTimeStamp()
};
conversationHistory[key] = history;
}
return history;
}
private void PruneInactiveConversationHistoryIfNeeded()
{
if (!configData.EnableHistory || configData.HistoryPruneMinutes <= 0 || conversationHistory.Count == 0)
{
return;
}
var now = GetTimeStamp();
if (now < nextHistoryPruneAt)
{
return;
}
nextHistoryPruneAt = now + Math.Min(configData.HistoryPruneMinutes * 60.0, 300.0);
PruneInactiveConversationHistory(now);
}
private void PruneInactiveConversationHistory(double now)
{
var expirationAge = configData.HistoryPruneMinutes * 60.0;
var keysToRemove = new List<string>();
foreach (var entry in conversationHistory)
{
if (now - entry.Value.LastUpdated >= expirationAge)
{
keysToRemove.Add(entry.Key);
}
}
foreach (var key in keysToRemove)
{
conversationHistory.Remove(key);
}
}
private void RemovePlayerConversationHistory(string playerId)
{
if (conversationHistory.Count == 0 || string.IsNullOrEmpty(playerId))
{
return;
}
var keysToRemove = new List<string>();
foreach (var entry in conversationHistory)
{
if (ConversationKeyContainsPlayer(entry.Key, playerId))
{
keysToRemove.Add(entry.Key);
}
}
foreach (var key in keysToRemove)
{
conversationHistory.Remove(key);
}
}
private static bool ConversationKeyContainsPlayer(string conversationKey, string playerId)
{
if (string.IsNullOrEmpty(conversationKey) || string.IsNullOrEmpty(playerId))
{
return false;
}
var separatorIndex = conversationKey.IndexOf('|');
if (separatorIndex < 0)
{
return false;
}
return string.CompareOrdinal(conversationKey, 0, playerId, 0, playerId.Length) == 0 &&
(conversationKey.Length == playerId.Length || separatorIndex == playerId.Length)
||
separatorIndex + 1 + playerId.Length == conversationKey.Length &&
string.CompareOrdinal(conversationKey, separatorIndex + 1, playerId, 0, playerId.Length) == 0;
}
private static string GetConversationKey(string playerA, string playerB)
{
return string.CompareOrdinal(playerA, playerB) < 0
? playerA + "|" + playerB
: playerB + "|" + playerA;
}
#endregion
#region Audit Logging
private void WriteAuditLog(IPlayer initiator, IPlayer target, string message)
{
try
{
var timestamp = DateTime.Now;
var filePath = Path.Combine(auditLogDirectoryPath, string.Format("privatemessages_{0:yyyy-MM-dd}.log", timestamp));
var line = string.Format(
"[{0:yyyy-MM-dd HH:mm:ss}] {1} ({2}) -> {3} ({4}): {5}",
timestamp,
SanitizeLogValue(initiator?.Name),
SanitizeLogValue(initiator?.Id),
SanitizeLogValue(target?.Name),
SanitizeLogValue(target?.Id),
SanitizeLogValue(message));
File.AppendAllText(filePath, line + Environment.NewLine);
}
catch (Exception ex)
{
PrintWarning("Failed to write PM audit log: {0}", ex.Message);
}
}
private void ClearAuditLogs()
{
try
{
if (!Directory.Exists(auditLogDirectoryPath))
{
return;
}
foreach (var filePath in Directory.GetFiles(auditLogDirectoryPath, "*.log", SearchOption.TopDirectoryOnly))
{
File.Delete(filePath);
}
}
catch (Exception ex)
{
PrintWarning("Failed to clear PM audit logs: {0}", ex.Message);
}
}
private void RemoveStaleReplyTargets(string playerId)
{
var staleReplyTargets = new List<string>();
foreach (var entry in pmHistory)
{
if (entry.Value == playerId)
{
staleReplyTargets.Add(entry.Key);
}
}
foreach (var stalePlayerId in staleReplyTargets)
{
pmHistory.Remove(stalePlayerId);
}
}
private void RemovePlayerSelections(string playerId)
{
var staleSelections = new List<string>();
foreach (var entry in selectedPmTargets)
{
if (entry.Value == playerId)
{
staleSelections.Add(entry.Key);
}
}
foreach (var stalePlayerId in staleSelections)
{
selectedPmTargets.Remove(stalePlayerId);
}
}
private static string SanitizeLogValue(string value)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
return value.Replace("\r", " ").Replace("\n", " ").Trim();
}
#endregion
#region Utilities
private static string RemoveRichText(string message)
{
if (string.IsNullOrEmpty(message))
{
return string.Empty;
}
var output = new char[message.Length];
var outputIndex = 0;
var insideTag = false;
for (var i = 0; i < message.Length; i++)
{
var character = message[i];
if (character == '<')
{
insideTag = true;
continue;
}
if (character == '>')
{
insideTag = false;
continue;
}
if (!insideTag)
{
output[outputIndex++] = character;
}
}
return new string(output, 0, outputIndex);
}
private static double GetTimeStamp()
{
return (DateTime.UtcNow - UnixEpoch).TotalSeconds;
}
#endregion
#region Configuration
private class ConfigData
{
public bool UseUFilter = false;
public bool UseIgnore = false;
public bool UseCooldown = false;
public bool UseBetterChatMute = false;
public bool EnableConsoleLogging = false;
public bool EnableFileAuditLogging = true;
public bool ClearAuditLogsOnNewSave = false;
public bool EnableHistory = false;
public bool UsePermission = false;
public int CooldownTime = 3;
public int HistoryPruneMinutes = 60;
public string PmCommand = "pm";
}
private void LoadVariables()
{
var configChanged = false;
configData = new ConfigData
{
UseUFilter = GetConfigValue(nameof(ConfigData.UseUFilter), false, ref configChanged),
UseIgnore = GetConfigValue(nameof(ConfigData.UseIgnore), false, ref configChanged),
UseCooldown = GetConfigValue(nameof(ConfigData.UseCooldown), false, ref configChanged),
UseBetterChatMute = GetConfigValue(nameof(ConfigData.UseBetterChatMute), false, ref configChanged),
EnableConsoleLogging = GetConfigValue(nameof(ConfigData.EnableConsoleLogging), false, ref configChanged),
EnableFileAuditLogging = GetConfigValue(nameof(ConfigData.EnableFileAuditLogging), true, ref configChanged),
ClearAuditLogsOnNewSave = GetConfigValue(nameof(ConfigData.ClearAuditLogsOnNewSave), false, ref configChanged),
EnableHistory = GetConfigValue(nameof(ConfigData.EnableHistory), false, ref configChanged),
UsePermission = GetConfigValue(nameof(ConfigData.UsePermission), false, ref configChanged),
CooldownTime = GetConfigValue(nameof(ConfigData.CooldownTime), 3, ref configChanged),
HistoryPruneMinutes = GetConfigValue(nameof(ConfigData.HistoryPruneMinutes), 60, ref configChanged),
PmCommand = GetConfigValue(nameof(ConfigData.PmCommand), "pm", ref configChanged)
};
if (configData.CooldownTime < 0)
{
configData.CooldownTime = 0;
configChanged = true;
}
if (configData.HistoryPruneMinutes < 0)
{
configData.HistoryPruneMinutes = 0;
configChanged = true;
}
if (string.IsNullOrEmpty(configData.PmCommand))
{
configData.PmCommand = "pm";
configChanged = true;
}
if (configChanged)
{
SaveConfig(configData);
}
}
protected override void LoadDefaultConfig()
{
SaveConfig(new ConfigData());
}
private T GetConfigValue<T>(string key, T defaultValue, ref bool configChanged)
{
if (Config[key] == null)
{
Config[key] = defaultValue;
configChanged = true;
return defaultValue;
}
return (T)Convert.ChangeType(Config[key], typeof(T));
}
private void SaveConfig(ConfigData config)
{
Config.WriteObject(config, true);
}
#endregion
}
}