]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Rate limit ahelps (#29219)
authorPieter-Jan Briers <pieterjan.briers+git@gmail.com>
Thu, 20 Jun 2024 22:13:02 +0000 (00:13 +0200)
committerGitHub <noreply@github.com>
Thu, 20 Jun 2024 22:13:02 +0000 (00:13 +0200)
* Make chat rate limits a general-purpose system.

Intending to use this with ahelps next.

* Rate limt ahelps

Fixes #28762

* Review comments

Content.Server/Administration/Systems/BwoinkSystem.cs
Content.Server/Chat/Managers/ChatManager.RateLimit.cs
Content.Server/Chat/Managers/ChatManager.cs
Content.Server/Chat/Managers/IChatManager.cs
Content.Server/Chat/Systems/ChatSystem.cs
Content.Server/Entry/EntryPoint.cs
Content.Server/IoC/ServerContentIoC.cs
Content.Server/Players/RateLimiting/PlayerRateLimitManager.cs [new file with mode: 0644]
Content.Shared.Database/LogType.cs
Content.Shared/CCVar/CCVars.cs
Resources/Locale/en-US/administration/bwoink.ftl

index a07115544bfd28c4a1ab15eb247be8d553fa9fcb..0a797aa02a8d64b6c0f799ec2b1939240258c5a1 100644 (file)
@@ -9,6 +9,7 @@ using Content.Server.Administration.Managers;
 using Content.Server.Afk;
 using Content.Server.Discord;
 using Content.Server.GameTicking;
+using Content.Server.Players.RateLimiting;
 using Content.Shared.Administration;
 using Content.Shared.CCVar;
 using Content.Shared.Mind;
@@ -27,6 +28,8 @@ namespace Content.Server.Administration.Systems
     [UsedImplicitly]
     public sealed partial class BwoinkSystem : SharedBwoinkSystem
     {
+        private const string RateLimitKey = "AdminHelp";
+
         [Dependency] private readonly IPlayerManager _playerManager = default!;
         [Dependency] private readonly IAdminManager _adminManager = default!;
         [Dependency] private readonly IConfigurationManager _config = default!;
@@ -35,6 +38,7 @@ namespace Content.Server.Administration.Systems
         [Dependency] private readonly GameTicker _gameTicker = default!;
         [Dependency] private readonly SharedMindSystem _minds = default!;
         [Dependency] private readonly IAfkManager _afkManager = default!;
+        [Dependency] private readonly PlayerRateLimitManager _rateLimit = default!;
 
         [GeneratedRegex(@"^https://discord\.com/api/webhooks/(\d+)/((?!.*/).*)$")]
         private static partial Regex DiscordRegex();
@@ -80,6 +84,22 @@ namespace Content.Server.Administration.Systems
 
             SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameRunLevelChanged);
             SubscribeNetworkEvent<BwoinkClientTypingUpdated>(OnClientTypingUpdated);
+
+            _rateLimit.Register(
+                RateLimitKey,
+                new RateLimitRegistration
+                {
+                    CVarLimitPeriodLength = CCVars.AhelpRateLimitPeriod,
+                    CVarLimitCount = CCVars.AhelpRateLimitCount,
+                    PlayerLimitedAction = PlayerRateLimitedAction
+                });
+        }
+
+        private void PlayerRateLimitedAction(ICommonSession obj)
+        {
+            RaiseNetworkEvent(
+                new BwoinkTextMessage(obj.UserId, default, Loc.GetString("bwoink-system-rate-limited"), playSound: false),
+                obj.Channel);
         }
 
         private void OnOverrideChanged(string obj)
@@ -395,6 +415,9 @@ namespace Content.Server.Administration.Systems
                 return;
             }
 
+            if (_rateLimit.CountAction(eventArgs.SenderSession, RateLimitKey) != RateLimitStatus.Allowed)
+                return;
+
             var escapedText = FormattedMessage.EscapeText(message.Text);
 
             string bwoinkText;
index cf87ab6322de111d277be0cb09b00491548e3145..45e7d2e20d0f26c732eb95a5ac5185d001e7adce 100644 (file)
@@ -1,84 +1,41 @@
-using System.Runtime.InteropServices;
+using Content.Server.Players.RateLimiting;
 using Content.Shared.CCVar;
 using Content.Shared.Database;
-using Robust.Shared.Enums;
 using Robust.Shared.Player;
-using Robust.Shared.Timing;
 
 namespace Content.Server.Chat.Managers;
 
 internal sealed partial class ChatManager
 {
-    private readonly Dictionary<ICommonSession, RateLimitDatum> _rateLimitData = new();
+    private const string RateLimitKey = "Chat";
 
-    public bool HandleRateLimit(ICommonSession player)
+    private void RegisterRateLimits()
     {
-        ref var datum = ref CollectionsMarshal.GetValueRefOrAddDefault(_rateLimitData, player, out _);
-        var time = _gameTiming.RealTime;
-        if (datum.CountExpires < time)
-        {
-            // Period expired, reset it.
-            var periodLength = _configurationManager.GetCVar(CCVars.ChatRateLimitPeriod);
-            datum.CountExpires = time + TimeSpan.FromSeconds(periodLength);
-            datum.Count = 0;
-            datum.Announced = false;
-        }
-
-        var maxCount = _configurationManager.GetCVar(CCVars.ChatRateLimitCount);
-        datum.Count += 1;
-
-        if (datum.Count <= maxCount)
-            return true;
-
-        // Breached rate limits, inform admins if configured.
-        if (_configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdmins))
-        {
-            if (datum.NextAdminAnnounce < time)
+        _rateLimitManager.Register(RateLimitKey,
+            new RateLimitRegistration
             {
-                SendAdminAlert(Loc.GetString("chat-manager-rate-limit-admin-announcement", ("player", player.Name)));
-                var delay = _configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdminsDelay);
-                datum.NextAdminAnnounce = time + TimeSpan.FromSeconds(delay);
-            }
-        }
-
-        if (!datum.Announced)
-        {
-            DispatchServerMessage(player, Loc.GetString("chat-manager-rate-limited"), suppressLog: true);
-            _adminLogger.Add(LogType.ChatRateLimited, LogImpact.Medium, $"Player {player} breached chat rate limits");
-
-            datum.Announced = true;
-        }
-
-        return false;
+                CVarLimitPeriodLength = CCVars.ChatRateLimitPeriod,
+                CVarLimitCount = CCVars.ChatRateLimitCount,
+                CVarAdminAnnounceDelay = CCVars.ChatRateLimitAnnounceAdminsDelay,
+                PlayerLimitedAction = RateLimitPlayerLimited,
+                AdminAnnounceAction = RateLimitAlertAdmins,
+                AdminLogType = LogType.ChatRateLimited,
+            });
     }
 
-    private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e)
+    private void RateLimitPlayerLimited(ICommonSession player)
     {
-        if (e.NewStatus == SessionStatus.Disconnected)
-            _rateLimitData.Remove(e.Session);
+        DispatchServerMessage(player, Loc.GetString("chat-manager-rate-limited"), suppressLog: true);
     }
 
-    private struct RateLimitDatum
+    private void RateLimitAlertAdmins(ICommonSession player)
     {
-        /// <summary>
-        /// Time stamp (relative to <see cref="IGameTiming.RealTime"/>) this rate limit period will expire at.
-        /// </summary>
-        public TimeSpan CountExpires;
-
-        /// <summary>
-        /// How many messages have been sent in the current rate limit period.
-        /// </summary>
-        public int Count;
-
-        /// <summary>
-        /// Have we announced to the player that they've been blocked in this rate limit period?
-        /// </summary>
-        public bool Announced;
+        if (_configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdmins))
+            SendAdminAlert(Loc.GetString("chat-manager-rate-limit-admin-announcement", ("player", player.Name)));
+    }
 
-        /// <summary>
-        /// Time stamp (relative to <see cref="IGameTiming.RealTime"/>) of the
-        /// next time we can send an announcement to admins about rate limit breach.
-        /// </summary>
-        public TimeSpan NextAdminAnnounce;
+    public RateLimitStatus HandleRateLimit(ICommonSession player)
+    {
+        return _rateLimitManager.CountAction(player, RateLimitKey);
     }
 }
index 79683db64117cbada394710fa99198c223b13ec0..6bb552d9769f8dcafed0eec5dda331a5a82357da 100644 (file)
@@ -5,18 +5,17 @@ using Content.Server.Administration.Logs;
 using Content.Server.Administration.Managers;
 using Content.Server.Administration.Systems;
 using Content.Server.MoMMI;
+using Content.Server.Players.RateLimiting;
 using Content.Server.Preferences.Managers;
 using Content.Shared.Administration;
 using Content.Shared.CCVar;
 using Content.Shared.Chat;
 using Content.Shared.Database;
 using Content.Shared.Mind;
-using Robust.Server.Player;
 using Robust.Shared.Configuration;
 using Robust.Shared.Network;
 using Robust.Shared.Player;
 using Robust.Shared.Replays;
-using Robust.Shared.Timing;
 using Robust.Shared.Utility;
 
 namespace Content.Server.Chat.Managers
@@ -43,8 +42,7 @@ namespace Content.Server.Chat.Managers
         [Dependency] private readonly IConfigurationManager _configurationManager = default!;
         [Dependency] private readonly INetConfigurationManager _netConfigManager = default!;
         [Dependency] private readonly IEntityManager _entityManager = default!;
-        [Dependency] private readonly IGameTiming _gameTiming = default!;
-        [Dependency] private readonly IPlayerManager _playerManager = default!;
+        [Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;
 
         /// <summary>
         /// The maximum length a player-sent message can be sent
@@ -64,7 +62,7 @@ namespace Content.Server.Chat.Managers
             _configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
             _configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
 
-            _playerManager.PlayerStatusChanged += PlayerStatusChanged;
+            RegisterRateLimits();
         }
 
         private void OnOocEnabledChanged(bool val)
@@ -206,7 +204,7 @@ namespace Content.Server.Chat.Managers
         /// <param name="type">The type of message.</param>
         public void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type)
         {
-            if (!HandleRateLimit(player))
+            if (HandleRateLimit(player) != RateLimitStatus.Allowed)
                 return;
 
             // Check if message exceeds the character limit
index c8c057a1ad760f3fda91c9544093d169d599a5d0..15d1288ee2357ded917a5895e4d26131887793f3 100644 (file)
@@ -1,4 +1,6 @@
 using System.Diagnostics.CodeAnalysis;
+using Content.Server.Players;
+using Content.Server.Players.RateLimiting;
 using Content.Shared.Administration;
 using Content.Shared.Chat;
 using Robust.Shared.Network;
@@ -50,6 +52,6 @@ namespace Content.Server.Chat.Managers
         /// </summary>
         /// <param name="player">The player sending a chat message.</param>
         /// <returns>False if the player has violated rate limits and should be blocked from sending further messages.</returns>
-        bool HandleRateLimit(ICommonSession player);
+        RateLimitStatus HandleRateLimit(ICommonSession player);
     }
 }
index 65977c74f51af0672f3e42793650e70bf0d721b3..55beaf1f7f5c506f2d81a3483d3a94c6ee3c47ac 100644 (file)
@@ -6,6 +6,7 @@ using Content.Server.Administration.Managers;
 using Content.Server.Chat.Managers;
 using Content.Server.Examine;
 using Content.Server.GameTicking;
+using Content.Server.Players.RateLimiting;
 using Content.Server.Speech.Components;
 using Content.Server.Speech.EntitySystems;
 using Content.Server.Station.Components;
@@ -183,7 +184,7 @@ public sealed partial class ChatSystem : SharedChatSystem
             return;
         }
 
-        if (player != null && !_chatManager.HandleRateLimit(player))
+        if (player != null && _chatManager.HandleRateLimit(player) != RateLimitStatus.Allowed)
             return;
 
         // Sus
@@ -272,7 +273,7 @@ public sealed partial class ChatSystem : SharedChatSystem
         if (!CanSendInGame(message, shell, player))
             return;
 
-        if (player != null && !_chatManager.HandleRateLimit(player))
+        if (player != null && _chatManager.HandleRateLimit(player) != RateLimitStatus.Allowed)
             return;
 
         // It doesn't make any sense for a non-player to send in-game OOC messages, whereas non-players may be sending
index e5b2338c5b7b0aec368de1d5be2efc72845e7a5d..3a9d07126e5b9f0036c6bdbe535a1eb9c96bf97d 100644 (file)
@@ -14,8 +14,10 @@ using Content.Server.Info;
 using Content.Server.IoC;
 using Content.Server.Maps;
 using Content.Server.NodeContainer.NodeGroups;
+using Content.Server.Players;
 using Content.Server.Players.JobWhitelist;
 using Content.Server.Players.PlayTimeTracking;
+using Content.Server.Players.RateLimiting;
 using Content.Server.Preferences.Managers;
 using Content.Server.ServerInfo;
 using Content.Server.ServerUpdates;
@@ -108,6 +110,7 @@ namespace Content.Server.Entry
                 _updateManager.Initialize();
                 _playTimeTracking.Initialize();
                 IoCManager.Resolve<JobWhitelistManager>().Initialize();
+                IoCManager.Resolve<PlayerRateLimitManager>().Initialize();
             }
         }
 
index c6dfcadd3826e0d3410a7b6a3779b772015fd74a..858ad2fe264f4e903df9f1a6f38f7a4f975b3019 100644 (file)
@@ -13,8 +13,10 @@ using Content.Server.Info;
 using Content.Server.Maps;
 using Content.Server.MoMMI;
 using Content.Server.NodeContainer.NodeGroups;
+using Content.Server.Players;
 using Content.Server.Players.JobWhitelist;
 using Content.Server.Players.PlayTimeTracking;
+using Content.Server.Players.RateLimiting;
 using Content.Server.Preferences.Managers;
 using Content.Server.ServerInfo;
 using Content.Server.ServerUpdates;
@@ -63,6 +65,7 @@ namespace Content.Server.IoC
             IoCManager.Register<ISharedPlaytimeManager, PlayTimeTrackingManager>();
             IoCManager.Register<ServerApi>();
             IoCManager.Register<JobWhitelistManager>();
+            IoCManager.Register<PlayerRateLimitManager>();
         }
     }
 }
diff --git a/Content.Server/Players/RateLimiting/PlayerRateLimitManager.cs b/Content.Server/Players/RateLimiting/PlayerRateLimitManager.cs
new file mode 100644 (file)
index 0000000..59f086f
--- /dev/null
@@ -0,0 +1,254 @@
+using System.Runtime.InteropServices;
+using Content.Server.Administration.Logs;
+using Content.Shared.Database;
+using Robust.Server.Player;
+using Robust.Shared.Configuration;
+using Robust.Shared.Enums;
+using Robust.Shared.Player;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Players.RateLimiting;
+
+/// <summary>
+/// General-purpose system to rate limit actions taken by clients, such as chat messages.
+/// </summary>
+/// <remarks>
+/// <para>
+/// Different categories of rate limits must be registered ahead of time by calling <see cref="Register"/>.
+/// Once registered, you can simply call <see cref="CountAction"/> to count a rate-limited action for a player.
+/// </para>
+/// <para>
+/// This system is intended for rate limiting player actions over short periods,
+/// to ward against spam that can cause technical issues such as admin client load.
+/// It should not be used for in-game actions or similar.
+/// </para>
+/// <para>
+/// Rate limits are reset when a client reconnects.
+/// This should not be an issue for the reasonably short rate limit periods this system is intended for.
+/// </para>
+/// </remarks>
+/// <seealso cref="RateLimitRegistration"/>
+public sealed class PlayerRateLimitManager
+{
+    [Dependency] private readonly IAdminLogManager _adminLog = default!;
+    [Dependency] private readonly IGameTiming _gameTiming = default!;
+    [Dependency] private readonly IConfigurationManager _cfg = default!;
+    [Dependency] private readonly IPlayerManager _playerManager = default!;
+
+    private readonly Dictionary<string, RegistrationData> _registrations = new();
+    private readonly Dictionary<ICommonSession, Dictionary<string, RateLimitDatum>> _rateLimitData = new();
+
+    /// <summary>
+    /// Count and validate an action performed by a player against rate limits.
+    /// </summary>
+    /// <param name="player">The player performing the action.</param>
+    /// <param name="key">The key string that was previously used to register a rate limit category.</param>
+    /// <returns>Whether the action counted should be blocked due to surpassing rate limits or not.</returns>
+    /// <exception cref="ArgumentException">
+    /// <paramref name="player"/> is not a connected player
+    /// OR <paramref name="key"/> is not a registered rate limit category.
+    /// </exception>
+    /// <seealso cref="Register"/>
+    public RateLimitStatus CountAction(ICommonSession player, string key)
+    {
+        if (player.Status == SessionStatus.Disconnected)
+            throw new ArgumentException("Player is not connected");
+        if (!_registrations.TryGetValue(key, out var registration))
+            throw new ArgumentException($"Unregistered key: {key}");
+
+        var playerData = _rateLimitData.GetOrNew(player);
+        ref var datum = ref CollectionsMarshal.GetValueRefOrAddDefault(playerData, key, out _);
+        var time = _gameTiming.RealTime;
+        if (datum.CountExpires < time)
+        {
+            // Period expired, reset it.
+            datum.CountExpires = time + registration.LimitPeriod;
+            datum.Count = 0;
+            datum.Announced = false;
+        }
+
+        datum.Count += 1;
+
+        if (datum.Count <= registration.LimitCount)
+            return RateLimitStatus.Allowed;
+
+        // Breached rate limits, inform admins if configured.
+        if (registration.AdminAnnounceDelay is { } cvarAnnounceDelay)
+        {
+            if (datum.NextAdminAnnounce < time)
+            {
+                registration.Registration.AdminAnnounceAction!(player);
+                datum.NextAdminAnnounce = time + cvarAnnounceDelay;
+            }
+        }
+
+        if (!datum.Announced)
+        {
+            registration.Registration.PlayerLimitedAction(player);
+            _adminLog.Add(
+                registration.Registration.AdminLogType,
+                LogImpact.Medium,
+                $"Player {player} breached '{key}' rate limit ");
+
+            datum.Announced = true;
+        }
+
+        return RateLimitStatus.Blocked;
+    }
+
+    /// <summary>
+    /// Register a new rate limit category.
+    /// </summary>
+    /// <param name="key">
+    /// The key string that will be referred to later with <see cref="CountAction"/>.
+    /// Must be unique and should probably just be a constant somewhere.
+    /// </param>
+    /// <param name="registration">The data specifying the rate limit's parameters.</param>
+    /// <exception cref="InvalidOperationException"><paramref name="key"/> has already been registered.</exception>
+    /// <exception cref="ArgumentException"><paramref name="registration"/> is invalid.</exception>
+    public void Register(string key, RateLimitRegistration registration)
+    {
+        if (_registrations.ContainsKey(key))
+            throw new InvalidOperationException($"Key already registered: {key}");
+
+        var data = new RegistrationData
+        {
+            Registration = registration,
+        };
+
+        if ((registration.AdminAnnounceAction == null) != (registration.CVarAdminAnnounceDelay == null))
+        {
+            throw new ArgumentException(
+                $"Must set either both {nameof(registration.AdminAnnounceAction)} and {nameof(registration.CVarAdminAnnounceDelay)} or neither");
+        }
+
+        _cfg.OnValueChanged(
+            registration.CVarLimitCount,
+            i => data.LimitCount = i,
+            invokeImmediately: true);
+        _cfg.OnValueChanged(
+            registration.CVarLimitPeriodLength,
+            i => data.LimitPeriod = TimeSpan.FromSeconds(i),
+            invokeImmediately: true);
+
+        if (registration.CVarAdminAnnounceDelay != null)
+        {
+            _cfg.OnValueChanged(
+                registration.CVarLimitCount,
+                i => data.AdminAnnounceDelay = TimeSpan.FromSeconds(i),
+                invokeImmediately: true);
+        }
+
+        _registrations.Add(key, data);
+    }
+
+    /// <summary>
+    /// Initialize the manager's functionality at game startup.
+    /// </summary>
+    public void Initialize()
+    {
+        _playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
+    }
+
+    private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
+    {
+        if (e.NewStatus == SessionStatus.Disconnected)
+            _rateLimitData.Remove(e.Session);
+    }
+
+    private sealed class RegistrationData
+    {
+        public required RateLimitRegistration Registration { get; init; }
+        public TimeSpan LimitPeriod { get; set; }
+        public int LimitCount { get; set; }
+        public TimeSpan? AdminAnnounceDelay { get; set; }
+    }
+
+    private struct RateLimitDatum
+    {
+        /// <summary>
+        /// Time stamp (relative to <see cref="IGameTiming.RealTime"/>) this rate limit period will expire at.
+        /// </summary>
+        public TimeSpan CountExpires;
+
+        /// <summary>
+        /// How many actions have been done in the current rate limit period.
+        /// </summary>
+        public int Count;
+
+        /// <summary>
+        /// Have we announced to the player that they've been blocked in this rate limit period?
+        /// </summary>
+        public bool Announced;
+
+        /// <summary>
+        /// Time stamp (relative to <see cref="IGameTiming.RealTime"/>) of the
+        /// next time we can send an announcement to admins about rate limit breach.
+        /// </summary>
+        public TimeSpan NextAdminAnnounce;
+    }
+}
+
+/// <summary>
+/// Contains all data necessary to register a rate limit with <see cref="PlayerRateLimitManager.Register"/>.
+/// </summary>
+public sealed class RateLimitRegistration
+{
+    /// <summary>
+    /// CVar that controls the period over which the rate limit is counted, measured in seconds.
+    /// </summary>
+    public required CVarDef<int> CVarLimitPeriodLength { get; init; }
+
+    /// <summary>
+    /// CVar that controls how many actions are allowed in a single rate limit period.
+    /// </summary>
+    public required CVarDef<int> CVarLimitCount { get; init; }
+
+    /// <summary>
+    /// An action that gets invoked when this rate limit has been breached by a player.
+    /// </summary>
+    /// <remarks>
+    /// This can be used for informing players or taking administrative action.
+    /// </remarks>
+    public required Action<ICommonSession> PlayerLimitedAction { get; init; }
+
+    /// <summary>
+    /// CVar that controls the minimum delay between admin notifications, measured in seconds.
+    /// This can be omitted to have no admin notification system.
+    /// </summary>
+    /// <remarks>
+    /// If set, <see cref="AdminAnnounceAction"/> must be set too.
+    /// </remarks>
+    public CVarDef<int>? CVarAdminAnnounceDelay { get; init; }
+
+    /// <summary>
+    /// An action that gets invoked when a rate limit was breached and admins should be notified.
+    /// </summary>
+    /// <remarks>
+    /// If set, <see cref="CVarAdminAnnounceDelay"/> must be set too.
+    /// </remarks>
+    public Action<ICommonSession>? AdminAnnounceAction { get; init; }
+
+    /// <summary>
+    /// Log type used to log rate limit violations to the admin logs system.
+    /// </summary>
+    public LogType AdminLogType { get; init; } = LogType.RateLimited;
+}
+
+/// <summary>
+/// Result of a rate-limited operation.
+/// </summary>
+/// <seealso cref="PlayerRateLimitManager.CountAction"/>
+public enum RateLimitStatus : byte
+{
+    /// <summary>
+    /// The action was not blocked by the rate limit.
+    /// </summary>
+    Allowed,
+
+    /// <summary>
+    /// The action was blocked by the rate limit.
+    /// </summary>
+    Blocked,
+}
index f486a7416c789f87a0b7f4869a91d1854ac80676..33a5d30c6a9a033e2536b2d7c83757d70577d972 100644 (file)
@@ -96,5 +96,13 @@ public enum LogType
     ChatRateLimited = 87,
     AtmosTemperatureChanged = 88,
     DeviceNetwork = 89,
-    StoreRefund = 90
+    StoreRefund = 90,
+
+    /// <summary>
+    /// User was rate-limited for some spam action.
+    /// </summary>
+    /// <remarks>
+    /// This is a default value used by <c>PlayerRateLimitManager</c>, though users can use different log types.
+    /// </remarks>
+    RateLimited = 91,
 }
index f20ea21491dcf906196d0f74a443e1ffdc0d2335..1a1a7f02262d95b747a330626494236a0b31b1c9 100644 (file)
@@ -883,6 +883,25 @@ namespace Content.Shared.CCVar
         public static readonly CVarDef<bool> AdminBypassMaxPlayers =
             CVarDef.Create("admin.bypass_max_players", true, CVar.SERVERONLY);
 
+        /*
+         * AHELP
+         */
+
+        /// <summary>
+        /// Ahelp rate limit values are accounted in periods of this size (seconds).
+        /// After the period has passed, the count resets.
+        /// </summary>
+        /// <seealso cref="AhelpRateLimitCount"/>
+        public static readonly CVarDef<int> AhelpRateLimitPeriod =
+            CVarDef.Create("ahelp.rate_limit_period", 2, CVar.SERVERONLY);
+
+        /// <summary>
+        /// How many ahelp messages are allowed in a single rate limit period.
+        /// </summary>
+        /// <seealso cref="AhelpRateLimitPeriod"/>
+        public static readonly CVarDef<int> AhelpRateLimitCount =
+            CVarDef.Create("ahelp.rate_limit_count", 10, CVar.SERVERONLY);
+
         /*
          * Explosions
          */
index 474af89c268c17d5e81d93b75d5b28bba952ab99..3a92f58ad18bc18efc67dd51dc9806f1fb9abd5b 100644 (file)
@@ -14,3 +14,5 @@ bwoink-system-typing-indicator = {$players} {$count ->
 admin-bwoink-play-sound = Bwoink?
 
 bwoink-title-none-selected = None selected
+
+bwoink-system-rate-limited = System: you are sending messages too quickly.