How to make a Rust private messages plugin (with /ignore)
A private messaging plugin that adds /pm, /r and /ignore with a per-player cooldown and a configurable message length limit. It uses the OnPlayerDisconnected hook to clear reply targets and cooldown timers.
Rust ships with no way to send a direct message to another player, and global chat is a bad place for anything that isn't meant for everyone. This plugin adds a whisper system built on three chat commands: /pm <player> <message> to start a conversation, /r <message> to answer the last person who messaged you, and /ignore <player> to block someone permanently.
Replies are resolved from an in-memory map of last senders, so /r always targets the right player even if several people message you. Each player has a cooldown between messages and every message is trimmed to a maximum length, which keeps chat clean and discourages spam. Ignore lists are persisted through Oxide's data file system, so they survive restarts. Cooldowns are tracked with a timestamp and cleaned up when a player disconnects via OnPlayerDisconnected.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("PrivateMessages", "RustPluginGenerator", "1.0.0")]
[Description("Adds /pm, /r and /ignore chat commands with a cooldown and message length limit.")]
public class PrivateMessages : RustPlugin
{
private const string PermUse = "privatemessages.use";
private const string PermIgnore = "privatemessages.ignore";
private PluginConfig config;
private Dictionary<ulong, HashSet<ulong>> ignoreData = new Dictionary<ulong, HashSet<ulong>>();
private readonly Dictionary<ulong, ulong> lastSender = new Dictionary<ulong, ulong>();
private readonly Dictionary<ulong, float> lastSent = new Dictionary<ulong, float>();
private class PluginConfig
{
[JsonProperty("Cooldown seconds between messages")]
public float CooldownSeconds = 5f;
[JsonProperty("Maximum message length")]
public int MaxMessageLength = 200;
[JsonProperty("Show sender name to receiver")]
public bool ShowSenderName = true;
[JsonProperty("Allow messaging yourself")]
public bool AllowSelfMessage = false;
}
protected override void LoadDefaultConfig() => config = new PluginConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<PluginConfig>();
if (config == null) throw new Exception();
}
catch
{
PrintWarning("Configuration file is invalid, using defaults");
LoadDefaultConfig();
}
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(config);
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["NoPermission"] = "You do not have permission to use this command.",
["UsagePm"] = "Usage: /pm <player> <message>",
["UsageReply"] = "Usage: /r <message>",
["UsageIgnore"] = "Usage: /ignore <player>",
["PlayerNotFound"] = "No player found matching '{0}'.",
["MultipleMatches"] = "Several players match '{0}'. Be more specific.",
["SelfMessage"] = "You cannot message yourself.",
["Cooldown"] = "Wait {0} more second(s) before sending another message.",
["EmptyMessage"] = "The message is empty.",
["Delivered"] = "To {0}: {1}",
["Received"] = "From {0}: {1}",
["ReceivedAnonymous"] = "Message: {0}",
["BlockedByTarget"] = "{0} is ignoring you.",
["NotMessaged"] = "Nobody has messaged you recently.",
["TargetOffline"] = "{0} is no longer online.",
["IgnoreAdded"] = "You are now ignoring {0}.",
["IgnoreRemoved"] = "You are no longer ignoring {0}."
}, this);
}
private string Msg(string key, string userId = null, params object[] args)
=> string.Format(lang.GetMessage(key, this, userId), args);
private void Init()
{
permission.RegisterPermission(PermUse, this);
permission.RegisterPermission(PermIgnore, this);
LoadIgnoreData();
}
private void OnServerInitialized()
{
timer.Once(1f, LoadIgnoreData);
}
private void LoadIgnoreData()
{
var data = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, HashSet<ulong>>>(Name);
if (data != null) ignoreData = data;
}
private void Unload()
{
Interface.Oxide.DataFileSystem.WriteObject(Name, ignoreData);
}
[ChatCommand("pm")]
private void CmdPm(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
if (args == null || args.Length < 2)
{
player.ChatMessage(Msg("UsagePm", player.UserIDString));
return;
}
BasePlayer target = FindTarget(args[0], player);
if (target == null)
{
player.ChatMessage(Msg("PlayerNotFound", player.UserIDString, args[0]));
return;
}
if (target == player && !config.AllowSelfMessage)
{
player.ChatMessage(Msg("SelfMessage", player.UserIDString));
return;
}
string text = string.Join(" ", args, 1, args.Length - 1);
SendPrivate(player, target, text);
}
[ChatCommand("r")]
private void CmdReply(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
if (args == null || args.Length < 1)
{
player.ChatMessage(Msg("UsageReply", player.UserIDString));
return;
}
ulong senderId;
if (!lastSender.TryGetValue(player.userID.Get(), out senderId))
{
player.ChatMessage(Msg("NotMessaged", player.UserIDString));
return;
}
BasePlayer target = BasePlayer.FindByID(senderId);
if (target == null || !target.IsConnected)
{
player.ChatMessage(Msg("TargetOffline", player.UserIDString, senderId));
return;
}
SendPrivate(player, target, string.Join(" ", args));
}
[ChatCommand("ignore")]
private void CmdIgnore(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, PermIgnore))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
if (args == null || args.Length < 1)
{
player.ChatMessage(Msg("UsageIgnore", player.UserIDString));
return;
}
BasePlayer target = FindTarget(args[0], player);
if (target == null)
{
player.ChatMessage(Msg("PlayerNotFound", player.UserIDString, args[0]));
return;
}
ulong owner = player.userID.Get();
HashSet<ulong> set;
if (!ignoreData.TryGetValue(owner, out set))
{
set = new HashSet<ulong>();
ignoreData[owner] = set;
}
if (set.Contains(target.userID.Get()))
{
set.Remove(target.userID.Get());
player.ChatMessage(Msg("IgnoreRemoved", player.UserIDString, target.displayName));
}
else
{
set.Add(target.userID.Get());
player.ChatMessage(Msg("IgnoreAdded", player.UserIDString, target.displayName));
}
}
private BasePlayer FindTarget(string nameOrId, BasePlayer requester)
{
ulong id;
if (ulong.TryParse(nameOrId, out id))
{
BasePlayer byId = BasePlayer.FindByID(id);
if (byId != null) return byId;
}
BasePlayer exact = BasePlayer.FindAwakeOrSleeping(nameOrId);
if (exact != null) return exact;
BasePlayer match = null;
int count = 0;
foreach (BasePlayer p in BasePlayer.activePlayerList)
{
if (p == null || !p.IsConnected) continue;
if (p.displayName.IndexOf(nameOrId, StringComparison.OrdinalIgnoreCase) < 0) continue;
match = p;
count++;
}
if (count > 1)
{
requester.ChatMessage(Msg("MultipleMatches", requester.UserIDString, nameOrId));
return null;
}
return match;
}
private void SendPrivate(BasePlayer sender, BasePlayer target, string text)
{
if (string.IsNullOrEmpty(text) || text.Trim().Length == 0)
{
sender.ChatMessage(Msg("EmptyMessage", sender.UserIDString));
return;
}
ulong senderId = sender.userID.Get();
float now = Time.realtimeSinceStartup;
float last;
if (lastSent.TryGetValue(senderId, out last) && now - last < config.CooldownSeconds)
{
sender.ChatMessage(Msg("Cooldown", sender.UserIDString,
Math.Ceiling(config.CooldownSeconds - (now - last))));
return;
}
if (text.Length > config.MaxMessageLength)
text = text.Substring(0, config.MaxMessageLength);
HashSet<ulong> ignored;
if (ignoreData.TryGetValue(target.userID.Get(), out ignored) && ignored.Contains(senderId))
{
sender.ChatMessage(Msg("BlockedByTarget", sender.UserIDString, target.displayName));
return;
}
lastSent[senderId] = now;
lastSender[target.userID.Get()] = senderId;
sender.ChatMessage(Msg("Delivered", sender.UserIDString, target.displayName, text));
target.ChatMessage(config.ShowSenderName
? Msg("Received", target.UserIDString, sender.displayName, text)
: Msg("ReceivedAnonymous", target.UserIDString, text));
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player == null) return;
ulong userId = player.userID.Get();
lastSent.Remove(userId);
lastSender.Remove(userId);
}
}
}Hooks used: OnServerInitialized, OnPlayerDisconnected
Config file
Created automatically on first load at oxide/config/PrivateMessages.json.
{
"Cooldown seconds between messages": 5,
"Maximum message length": 200,
"Show sender name to receiver": true,
"Allow messaging yourself": false
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/pm <player> <message> | Sends a private message to a player matched by name or Steam ID. | privatemessages.use |
/r <message> | Replies to the last player who messaged you. | privatemessages.use |
/ignore <player> | Toggles the named player on your ignore list. | privatemessages.ignore |
| Permission | Grants |
|---|---|
privatemessages.use | Allows sending private messages with /pm and /r. |
privatemessages.ignore | Allows managing the ignore list with /ignore. |
How the code works
Config and default messages
PluginConfigholds the cooldown in seconds, the maximum message length and a switch that hides the sender's name from the receiver.LoadDefaultConfigcreates it,LoadConfigfalls back to defaults on a broken file, andLoadDefaultMessagesregisters every chat response — including the{0}placeholder for a player name — so the text can be translated or edited per language inoxide/lang.Resolving a name to a player
/pmaccepts a full name, a partial name or a Steam ID.FindTargetfirst triesBasePlayer.FindAwakeOrSleeping, then falls back to scanningBasePlayer.activePlayerListfor a case-insensitivedisplayNamematch. If more than one candidate matches, the command reports ambiguity instead of guessing. Self-targeting is rejected so players cannot message themselves.Sending and the cooldown gate
SendPrivateis the single path for both/pmand/r. It checkspermission.UserHasPermissionforprivatemessages.use, then comparesTime.realtimeSinceStartupagainst the stored timestamp inlastSentplusconfig.CooldownSeconds. On success it writes the new timestamp, records the sender inlastSenderon the receiver, and callsChatMessageon the target. The message body is trimmed toconfig.MaxMessageLengthcharacters before sending.Replying with /r
lastSenderis aDictionary<ulong, ulong>mapping a receiver to the player who last messaged them./rlooks up the caller's entry, finds that player withBasePlayer.FindByID, and hands the text to the sameSendPrivateroutine, so cooldown and length rules apply unchanged. If the entry is missing or the player is offline, the caller gets a lang message explaining why the reply failed.Ignore lists and persistence
/ignoretoggles the named player's ID in aHashSet<ulong>stored per owner inignoreData, aDictionary<ulong, HashSet<ulong>>. The dictionary is read inOnServerInitializedwithInterface.Oxide.DataFileSystem.ReadObjectand written inUnload. When a message is delivered, the receiver's set is checked and, if the sender is listed, the sender is told the message was blocked.Cleanup on disconnect
OnPlayerDisconnectedremoves the player's cooldown timestamp and theirlastSenderentry, so a reconnecting player starts with a fresh reply target rather than a stale ID. Ignore lists are intentionally kept, since they are meant to be long-lived. The hook only touches in-memory state, so it is cheap and safe to call on every disconnect.
Install it on your server
Save PrivateMessages.cs
Keep the file name exactly as the class name. Oxide matches them.Upload it to the plugins folder
Oxide:oxide/plugins/. Carbon:carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.Edit the config if you want
On first load the default config is written tooxide/config/PrivateMessages.json(Carbon:carbon/configs/PrivateMessages.json). After editing, reload it:oxide.reload PrivateMessages c.reload PrivateMessages # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default privatemessages.use c.grant group default privatemessages.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Admin-only broadcast
Add a console command that sends a private message to every online player through the same delivery routine.
Generate this versionTeam-aware replies
Warn the receiver before they reply when the target is not on their team.
Generate this versionStored message history
Keep the last few private messages per player and let them review them later.
Generate this version
FAQ
Why does /pm say the player is not found?
BasePlayer.FindAwakeOrSleeping only matches exact names or Steam IDs, and the fallback scan only looks at connected players. A partial match must be unique. If two players share the prefix, the plugin reports it instead of choosing one.
Does the cooldown apply to replies too?
Yes. /r and /pm both go through SendPrivate, so the same CooldownSeconds value gates them. Raise the value in oxide/config/PrivateMessages.json if conversations between two fast typers feel blocked.
Where are ignore lists stored?
In oxide/data/PrivateMessages.json under the owner's Steam ID, written when the plugin unloads or the server saves. Deleting that file resets every ignore list on the server.
How do I install it on Oxide or Carbon?
Drop PrivateMessages.cs into oxide/plugins, then run oxide.reload PrivateMessages from the server console or RCON. Carbon loads the same file from its plugins folder. Edit oxide/config/PrivateMessages.json and reload to apply new defaults.
Customize it for your server
Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More chat plugins
- Automatic announcementsAutoBroadcast sends a list of chat messages to every online player on a repeating timer, in order or random order, and fills {players} and {time}…OnServerInitialized
- Chat loggerRecords every chat message with timestamp, SteamID, name, channel and text into a capped oxide/data file, and adds a /chatlog command that prints a…OnServerInitializedOnPlayerChat
- Chat mentionsDetects @name tokens in chat and sends the mentioned online player a private notice plus a notification sound, with a per-sender cooldown.OnPlayerChatOnPlayerDisconnected
Last updated Oct 26, 2026.
Customize private messages
Customize