Skip to content
RustPluginGenerator

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

PrivateMessages.cs
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.

PrivateMessages.json
{
  "Cooldown seconds between messages": 5,
  "Maximum message length": 200,
  "Show sender name to receiver": true,
  "Allow messaging yourself": false
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/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
Permissions
PermissionGrants
privatemessages.useAllows sending private messages with /pm and /r.
privatemessages.ignoreAllows managing the ignore list with /ignore.

How the code works

  1. Config and default messages

    PluginConfig holds the cooldown in seconds, the maximum message length and a switch that hides the sender's name from the receiver. LoadDefaultConfig creates it, LoadConfig falls back to defaults on a broken file, and LoadDefaultMessages registers every chat response — including the {0} placeholder for a player name — so the text can be translated or edited per language in oxide/lang.

  2. Resolving a name to a player

    /pm accepts a full name, a partial name or a Steam ID. FindTarget first tries BasePlayer.FindAwakeOrSleeping, then falls back to scanning BasePlayer.activePlayerList for a case-insensitive displayName match. If more than one candidate matches, the command reports ambiguity instead of guessing. Self-targeting is rejected so players cannot message themselves.

  3. Sending and the cooldown gate

    SendPrivate is the single path for both /pm and /r. It checks permission.UserHasPermission for privatemessages.use, then compares Time.realtimeSinceStartup against the stored timestamp in lastSent plus config.CooldownSeconds. On success it writes the new timestamp, records the sender in lastSender on the receiver, and calls ChatMessage on the target. The message body is trimmed to config.MaxMessageLength characters before sending.

  4. Replying with /r

    lastSender is a Dictionary<ulong, ulong> mapping a receiver to the player who last messaged them. /r looks up the caller's entry, finds that player with BasePlayer.FindByID, and hands the text to the same SendPrivate routine, 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.

  5. Ignore lists and persistence

    /ignore toggles the named player's ID in a HashSet<ulong> stored per owner in ignoreData, a Dictionary<ulong, HashSet<ulong>>. The dictionary is read in OnServerInitialized with Interface.Oxide.DataFileSystem.ReadObject and written in Unload. 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.

  6. Cleanup on disconnect

    OnPlayerDisconnected removes the player's cooldown timestamp and their lastSender entry, 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

  1. Save PrivateMessages.cs

    Keep the file name exactly as the class name. Oxide matches them.
  2. 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.
  3. Edit the config if you want

    On first load the default config is written to oxide/config/PrivateMessages.json (Carbon: carbon/configs/PrivateMessages.json). After editing, reload it:oxide.reload PrivateMessages c.reload PrivateMessages # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip: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 version
  • Team-aware replies

    Warn the receiver before they reply when the target is not on their team.

    Generate this version
  • Stored 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.

Prompt: Create a Rust Oxide private messages plugin with /pm, /r and /ignore, a 5 second cooldown, 200 character limit, sender names shown, and ignore lists persisted to oxide/data.
Customize for my server

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026.

Customize private messages

Customize