--- /dev/null
+using System.Runtime.InteropServices;
+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();
+
+ public bool HandleRateLimit(ICommonSession player)
+ {
+ 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)
+ {
+ 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;
+ }
+
+ private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e)
+ {
+ if (e.NewStatus == SessionStatus.Disconnected)
+ _rateLimitData.Remove(e.Session);
+ }
+
+ 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 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;
+
+ /// <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;
+ }
+}
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
/// <summary>
/// Dispatches chat messages to clients.
/// </summary>
- internal sealed class ChatManager : IChatManager
+ internal sealed partial class ChatManager : IChatManager
{
private static readonly Dictionary<string, string> PatronOocColors = new()
{
[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!;
/// <summary>
/// The maximum length a player-sent message can be sent
_configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
+
+ _playerManager.PlayerStatusChanged += PlayerStatusChanged;
}
private void OnOocEnabledChanged(bool val)
/// <param name="type">The type of message.</param>
public void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type)
{
+ if (!HandleRateLimit(player))
+ return;
+
// Check if message exceeds the character limit
if (message.Length > MaxMessageLength)
{
[return: NotNullIfNotNull(nameof(author))]
ChatUser? EnsurePlayer(NetUserId? author);
+
+ /// <summary>
+ /// Called when a player sends a chat message to handle rate limits.
+ /// Will update counts and do necessary actions if breached.
+ /// </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);
}
}
return;
}
+ if (player != null && !_chatManager.HandleRateLimit(player))
+ return;
+
// Sus
if (player?.AttachedEntity is { Valid: true } entity && source != entity)
{
if (!CanSendInGame(message, shell, player))
return;
+ if (player != null && !_chatManager.HandleRateLimit(player))
+ return;
+
// It doesn't make any sense for a non-player to send in-game OOC messages, whereas non-players may be sending
// in-game IC messages.
if (player?.AttachedEntity is not { Valid: true } entity || source != entity)
ItemConfigure = 84,
DeviceLinking = 85,
Tile = 86,
+
+ /// <summary>
+ /// A client has sent too many chat messages recently and is temporarily blocked from sending more.
+ /// </summary>
+ ChatRateLimited = 87,
}
* CHAT
*/
+ /// <summary>
+ /// Chat rate limit values are accounted in periods of this size (seconds).
+ /// After the period has passed, the count resets.
+ /// </summary>
+ /// <seealso cref="ChatRateLimitCount"/>
+ public static readonly CVarDef<int> ChatRateLimitPeriod =
+ CVarDef.Create("chat.rate_limit_period", 2, CVar.SERVERONLY);
+
+ /// <summary>
+ /// How many chat messages are allowed in a single rate limit period.
+ /// </summary>
+ /// <remarks>
+ /// The total rate limit throughput per second is effectively
+ /// <see cref="ChatRateLimitCount"/> divided by <see cref="ChatRateLimitCount"/>.
+ /// </remarks>
+ /// <seealso cref="ChatRateLimitPeriod"/>
+ /// <seealso cref="ChatRateLimitAnnounceAdmins"/>
+ public static readonly CVarDef<int> ChatRateLimitCount =
+ CVarDef.Create("chat.rate_limit_count", 10, CVar.SERVERONLY);
+
+ /// <summary>
+ /// If true, announce when a player breached chat rate limit to game administrators.
+ /// </summary>
+ /// <seealso cref="ChatRateLimitAnnounceAdminsDelay"/>
+ public static readonly CVarDef<bool> ChatRateLimitAnnounceAdmins =
+ CVarDef.Create("chat.rate_limit_announce_admins", true, CVar.SERVERONLY);
+
+ /// <summary>
+ /// Minimum delay (in seconds) between announcements from <see cref="ChatRateLimitAnnounceAdmins"/>.
+ /// </summary>
+ public static readonly CVarDef<int> ChatRateLimitAnnounceAdminsDelay =
+ CVarDef.Create("chat.rate_limit_announce_admins_delay", 15, CVar.SERVERONLY);
+
public static readonly CVarDef<int> ChatMaxMessageLength =
CVarDef.Create("chat.max_message_length", 1000, CVar.SERVER | CVar.REPLICATED);
chat-manager-dead-channel-name = DEAD
chat-manager-admin-channel-name = ADMIN
+chat-manager-rate-limited = You are sending messages too quickly!
+chat-manager-rate-limit-admin-announcement = Player { $player } breached chat rate limits. Watch them if this is a regular occurence.
+
## Speech verbs for chat
chat-speech-verb-suffix-exclamation = !