_sawmill.Level = LogLevel.Info;
}
+ public void SendAdminAlert(string message)
+ {
+ // See server-side manager. This just exists for shared code.
+ }
+
+ public void SendAdminAlert(EntityUid player, string message)
+ {
+ // See server-side manager. This just exists for shared code.
+ }
+
public void SendMessage(string text, ChatSelectChannel channel)
{
var str = text.ToString();
namespace Content.Client.Chat.Managers
{
- public interface IChatManager
+ public interface IChatManager : ISharedChatManager
{
- void Initialize();
-
public void SendMessage(string text, ChatSelectChannel channel);
}
}
using Content.Client.Voting;
using Content.Shared.Administration.Logs;
using Content.Client.Lobby;
+using Content.Client.Players.RateLimiting;
using Content.Shared.Administration.Managers;
+using Content.Shared.Chat;
using Content.Shared.Players.PlayTimeTracking;
+using Content.Shared.Players.RateLimiting;
namespace Content.Client.IoC
{
collection.Register<IParallaxManager, ParallaxManager>();
collection.Register<IChatManager, ChatManager>();
+ collection.Register<ISharedChatManager, ChatManager>();
collection.Register<IClientPreferencesManager, ClientPreferencesManager>();
collection.Register<IStylesheetManager, StylesheetManager>();
collection.Register<IScreenshotHook, ScreenshotHook>();
collection.Register<ExtendedDisconnectInformationManager>();
collection.Register<JobRequirementsManager>();
collection.Register<DocumentParsingManager>();
- collection.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
+ collection.Register<ContentReplayPlaybackManager>();
collection.Register<ISharedPlaytimeManager, JobRequirementsManager>();
collection.Register<MappingManager>();
collection.Register<DebugMonitorManager>();
+ collection.Register<PlayerRateLimitManager>();
+ collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
}
}
}
--- /dev/null
+using Content.Shared.Players.RateLimiting;
+using Robust.Shared.Player;
+
+namespace Content.Client.Players.RateLimiting;
+
+public sealed class PlayerRateLimitManager : SharedPlayerRateLimitManager
+{
+ public override RateLimitStatus CountAction(ICommonSession player, string key)
+ {
+ // TODO Rate-Limit
+ // Add support for rate limit prediction
+ // I.e., dont mis-predict just because somebody is clicking too quickly.
+ return RateLimitStatus.Allowed;
+ }
+
+ public override void Register(string key, RateLimitRegistration registration)
+ {
+ }
+
+ public override void Initialize()
+ {
+ }
+}
(CCVars.ConfigPresetDevelopment.Name, "false"),
(CCVars.AdminLogsEnabled.Name, "false"),
(CCVars.AutosaveEnabled.Name, "false"),
- (CVars.NetBufferSize.Name, "0")
+ (CVars.NetBufferSize.Name, "0"),
+ (CCVars.InteractionRateLimitCount.Name, "9999999"),
+ (CCVars.InteractionRateLimitPeriod.Name, "0.1"),
};
public static async Task SetupCVars(RobustIntegrationTest.IntegrationInstance instance, PoolSettings settings)
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
+using Content.Shared.Players.RateLimiting;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared;
_rateLimit.Register(
RateLimitKey,
- new RateLimitRegistration
- {
- CVarLimitPeriodLength = CCVars.AhelpRateLimitPeriod,
- CVarLimitCount = CCVars.AhelpRateLimitCount,
- PlayerLimitedAction = PlayerRateLimitedAction
- });
+ new RateLimitRegistration(CCVars.AhelpRateLimitPeriod,
+ CCVars.AhelpRateLimitCount,
+ PlayerRateLimitedAction)
+ );
}
private void PlayerRateLimitedAction(ICommonSession obj)
-using Content.Server.Players.RateLimiting;
using Content.Shared.CCVar;
using Content.Shared.Database;
+using Content.Shared.Players.RateLimiting;
using Robust.Shared.Player;
namespace Content.Server.Chat.Managers;
private void RegisterRateLimits()
{
_rateLimitManager.Register(RateLimitKey,
- new RateLimitRegistration
- {
- CVarLimitPeriodLength = CCVars.ChatRateLimitPeriod,
- CVarLimitCount = CCVars.ChatRateLimitCount,
- CVarAdminAnnounceDelay = CCVars.ChatRateLimitAnnounceAdminsDelay,
- PlayerLimitedAction = RateLimitPlayerLimited,
- AdminAnnounceAction = RateLimitAlertAdmins,
- AdminLogType = LogType.ChatRateLimited,
- });
+ new RateLimitRegistration(CCVars.ChatRateLimitPeriod,
+ CCVars.ChatRateLimitCount,
+ RateLimitPlayerLimited,
+ CCVars.ChatRateLimitAnnounceAdminsDelay,
+ RateLimitAlertAdmins,
+ LogType.ChatRateLimited)
+ );
}
private void RateLimitPlayerLimited(ICommonSession player)
private void RateLimitAlertAdmins(ICommonSession player)
{
- if (_configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdmins))
- SendAdminAlert(Loc.GetString("chat-manager-rate-limit-admin-announcement", ("player", player.Name)));
+ SendAdminAlert(Loc.GetString("chat-manager-rate-limit-admin-announcement", ("player", player.Name)));
}
public RateLimitStatus HandleRateLimit(ICommonSession player)
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Mind;
+using Content.Shared.Players.RateLimiting;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
using System.Diagnostics.CodeAnalysis;
-using Content.Server.Players;
-using Content.Server.Players.RateLimiting;
using Content.Shared.Administration;
using Content.Shared.Chat;
+using Content.Shared.Players.RateLimiting;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Content.Server.Chat.Managers
{
- public interface IChatManager
+ public interface IChatManager : ISharedChatManager
{
- void Initialize();
-
/// <summary>
/// Dispatch a server announcement to every connected player.
/// </summary>
void SendHookOOC(string sender, string message);
void SendAdminAnnouncement(string message, AdminFlags? flagBlacklist = null, AdminFlags? flagWhitelist = null);
void SendAdminAnnouncementMessage(ICommonSession player, string message, bool suppressLog = true);
- void SendAdminAlert(string message);
- void SendAdminAlert(EntityUid player, string message);
void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat,
INetChannel client, Color? colorOverride = null, bool recordReplay = false, string? audioPath = null, float audioVolume = 0, NetUserId? author = null);
using Content.Shared.IdentityManagement;
using Content.Shared.Mobs.Systems;
using Content.Shared.Players;
+using Content.Shared.Players.RateLimiting;
using Content.Shared.Radio;
using Content.Shared.Whitelist;
using Robust.Server.Player;
using Content.Server.Maps;
using Content.Server.MoMMI;
using Content.Server.NodeContainer.NodeGroups;
-using Content.Server.Objectives;
-using Content.Server.Players;
using Content.Server.Players.JobWhitelist;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Players.RateLimiting;
using Content.Server.Worldgen.Tools;
using Content.Shared.Administration.Logs;
using Content.Shared.Administration.Managers;
+using Content.Shared.Chat;
using Content.Shared.Kitchen;
using Content.Shared.Players.PlayTimeTracking;
+using Content.Shared.Players.RateLimiting;
namespace Content.Server.IoC
{
public static void Register()
{
IoCManager.Register<IChatManager, ChatManager>();
+ IoCManager.Register<ISharedChatManager, ChatManager>();
IoCManager.Register<IChatSanitizationManager, ChatSanitizationManager>();
IoCManager.Register<IMoMMILink, MoMMILink>();
IoCManager.Register<IServerPreferencesManager, ServerPreferencesManager>();
IoCManager.Register<ServerApi>();
IoCManager.Register<JobWhitelistManager>();
IoCManager.Register<PlayerRateLimitManager>();
+ IoCManager.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
IoCManager.Register<MappingManager>();
}
}
using System.Runtime.InteropServices;
using Content.Server.Administration.Logs;
using Content.Shared.Database;
+using Content.Shared.Players.RateLimiting;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
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
+public sealed class PlayerRateLimitManager : SharedPlayerRateLimitManager
{
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly IGameTiming _gameTiming = 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)
+ public override RateLimitStatus CountAction(ICommonSession player, string key)
{
if (player.Status == SessionStatus.Disconnected)
throw new ArgumentException("Player is not connected");
return RateLimitStatus.Allowed;
// Breached rate limits, inform admins if configured.
- if (registration.AdminAnnounceDelay is { } cvarAnnounceDelay)
+ // Negative delays can be used to disable admin announcements.
+ if (registration.AdminAnnounceDelay is {TotalSeconds: >= 0} cvarAnnounceDelay)
{
if (datum.NextAdminAnnounce < time)
{
if (!datum.Announced)
{
- registration.Registration.PlayerLimitedAction(player);
+ registration.Registration.PlayerLimitedAction?.Invoke(player);
_adminLog.Add(
registration.Registration.AdminLogType,
LogImpact.Medium,
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)
+ public override void Register(string key, RateLimitRegistration registration)
{
if (_registrations.ContainsKey(key))
throw new InvalidOperationException($"Key already registered: {key}");
if (registration.CVarAdminAnnounceDelay != null)
{
_cfg.OnValueChanged(
- registration.CVarLimitCount,
+ registration.CVarAdminAnnounceDelay,
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()
+ public override void Initialize()
{
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
}
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,
-}
/// 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);
+ public static readonly CVarDef<float> AhelpRateLimitPeriod =
+ CVarDef.Create("ahelp.rate_limit_period", 2f, CVar.SERVERONLY);
/// <summary>
/// How many ahelp messages are allowed in a single rate limit period.
/// 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);
+ public static readonly CVarDef<float> ChatRateLimitPeriod =
+ CVarDef.Create("chat.rate_limit_period", 2f, CVar.SERVERONLY);
/// <summary>
/// How many chat messages are allowed in a single rate limit period.
/// <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"/>.
+ /// Minimum delay (in seconds) between notifying admins about chat message rate limit violations.
+ /// A negative value disables admin announcements.
/// </summary>
public static readonly CVarDef<int> ChatRateLimitAnnounceAdminsDelay =
CVarDef.Create("chat.rate_limit_announce_admins_delay", 15, CVar.SERVERONLY);
public static readonly CVarDef<bool> ToggleWalk =
CVarDef.Create("control.toggle_walk", false, CVar.CLIENTONLY | CVar.ARCHIVE);
+ /*
+ * Interactions
+ */
+
+ // The rationale behind the default limit is simply that I can easily get to 7 interactions per second by just
+ // trying to spam toggle a light switch or lever (though the UseDelay component limits the actual effect of the
+ // interaction). I don't want to accidentally spam admins with alerts just because somebody is spamming a
+ // key manually, nor do we want to alert them just because the player is having network issues and the server
+ // receives multiple interactions at once. But we also want to try catch people with modified clients that spam
+ // many interactions on the same tick. Hence, a very short period, with a relatively high count.
+
+ /// <summary>
+ /// Maximum number of interactions that a player can perform within <see cref="InteractionRateLimitCount"/> seconds
+ /// </summary>
+ public static readonly CVarDef<int> InteractionRateLimitCount =
+ CVarDef.Create("interaction.rate_limit_count", 5, CVar.SERVER | CVar.REPLICATED);
+
+ /// <seealso cref="InteractionRateLimitCount"/>
+ public static readonly CVarDef<float> InteractionRateLimitPeriod =
+ CVarDef.Create("interaction.rate_limit_period", 0.5f, CVar.SERVER | CVar.REPLICATED);
+
+ /// <summary>
+ /// Minimum delay (in seconds) between notifying admins about interaction rate limit violations. A negative
+ /// value disables admin announcements.
+ /// </summary>
+ public static readonly CVarDef<int> InteractionRateLimitAnnounceAdminsDelay =
+ CVarDef.Create("interaction.rate_limit_announce_admins_delay", 120, CVar.SERVERONLY);
+
/*
* STORAGE
*/
--- /dev/null
+namespace Content.Shared.Chat;
+
+public interface ISharedChatManager
+{
+ void Initialize();
+ void SendAdminAlert(string message);
+ void SendAdminAlert(EntityUid player, string message);
+}
using System.Linq;
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
+using Content.Shared.CCVar;
+using Content.Shared.Chat;
using Content.Shared.CombatMode;
using Content.Shared.Database;
using Content.Shared.Ghost;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Pulling.Systems;
using Content.Shared.Physics;
+using Content.Shared.Players.RateLimiting;
using Content.Shared.Popups;
-using Content.Shared.Silicons.StationAi;
using Content.Shared.Storage;
using Content.Shared.Tag;
using Content.Shared.Timing;
using Content.Shared.Verbs;
using Content.Shared.Wall;
using JetBrains.Annotations;
+using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
+ [Dependency] private readonly SharedPlayerRateLimitManager _rateLimit = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly ISharedChatManager _chat = default!;
private EntityQuery<IgnoreUIRangeComponent> _ignoreUiRangeQuery;
private EntityQuery<FixturesComponent> _fixtureQuery;
public const float InteractionRange = 1.5f;
public const float InteractionRangeSquared = InteractionRange * InteractionRange;
-
public const float MaxRaycastRange = 100f;
+ public const string RateLimitKey = "Interaction";
public delegate bool Ignored(EntityUid entity);
new PointerInputCmdHandler(HandleTryPullObject))
.Register<SharedInteractionSystem>();
+ _rateLimit.Register(RateLimitKey,
+ new RateLimitRegistration(CCVars.InteractionRateLimitPeriod,
+ CCVars.InteractionRateLimitCount,
+ null,
+ CCVars.InteractionRateLimitAnnounceAdminsDelay,
+ RateLimitAlertAdmins)
+ );
+
InitializeBlocking();
}
+ private void RateLimitAlertAdmins(ICommonSession session)
+ {
+ _chat.SendAdminAlert(Loc.GetString("interaction-rate-limit-admin-announcement", ("player", session.Name)));
+ }
+
public override void Shutdown()
{
CommandBinds.Unregister<SharedInteractionSystem>();
return InRangeUnobstructed(user, wearer) && _containerSystem.IsInSameOrParentContainer(user, wearer);
}
- protected bool ValidateClientInput(ICommonSession? session, EntityCoordinates coords,
- EntityUid uid, [NotNullWhen(true)] out EntityUid? userEntity)
+ protected bool ValidateClientInput(
+ ICommonSession? session,
+ EntityCoordinates coords,
+ EntityUid uid,
+ [NotNullWhen(true)] out EntityUid? userEntity)
{
userEntity = null;
return false;
}
- return true;
+ return _rateLimit.CountAction(session!, RateLimitKey) == RateLimitStatus.Allowed;
}
/// <summary>
--- /dev/null
+using Content.Shared.Database;
+using Robust.Shared.Configuration;
+using Robust.Shared.Player;
+
+namespace Content.Shared.Players.RateLimiting;
+
+/// <summary>
+/// Contains all data necessary to register a rate limit with <see cref="SharedPlayerRateLimitManager.Register"/>.
+/// </summary>
+public sealed class RateLimitRegistration(
+ CVarDef<float> cVarLimitPeriodLength,
+ CVarDef<int> cVarLimitCount,
+ Action<ICommonSession>? playerLimitedAction,
+ CVarDef<int>? cVarAdminAnnounceDelay = null,
+ Action<ICommonSession>? adminAnnounceAction = null,
+ LogType adminLogType = LogType.RateLimited)
+{
+ /// <summary>
+ /// CVar that controls the period over which the rate limit is counted, measured in seconds.
+ /// </summary>
+ public readonly CVarDef<float> CVarLimitPeriodLength = cVarLimitPeriodLength;
+
+ /// <summary>
+ /// CVar that controls how many actions are allowed in a single rate limit period.
+ /// </summary>
+ public readonly CVarDef<int> CVarLimitCount = cVarLimitCount;
+
+ /// <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 readonly Action<ICommonSession>? PlayerLimitedAction = playerLimitedAction;
+
+ /// <summary>
+ /// CVar that controls the minimum delay between admin notifications, measured in seconds.
+ /// This can be omitted to have no admin notification system.
+ /// If the cvar is set to 0, there every breach will be reported.
+ /// If the cvar is set to a negative number, admin announcements are disabled.
+ /// </summary>
+ /// <remarks>
+ /// If set, <see cref="AdminAnnounceAction"/> must be set too.
+ /// </remarks>
+ public readonly CVarDef<int>? CVarAdminAnnounceDelay = cVarAdminAnnounceDelay;
+
+ /// <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 readonly Action<ICommonSession>? AdminAnnounceAction = adminAnnounceAction;
+
+ /// <summary>
+ /// Log type used to log rate limit violations to the admin logs system.
+ /// </summary>
+ public readonly LogType AdminLogType = adminLogType;
+}
+
+/// <summary>
+/// Result of a rate-limited operation.
+/// </summary>
+/// <seealso cref="SharedPlayerRateLimitManager.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,
+}
--- /dev/null
+using Robust.Shared.Player;
+
+namespace Content.Shared.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 abstract class SharedPlayerRateLimitManager
+{
+ /// <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 abstract RateLimitStatus CountAction(ICommonSession player, string key);
+
+ /// <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 abstract void Register(string key, RateLimitRegistration registration);
+
+ /// <summary>
+ /// Initialize the manager's functionality at game startup.
+ /// </summary>
+ public abstract void Initialize();
+}
shared-interaction-system-in-range-unobstructed-cannot-reach = You can't reach there!
-interaction-system-user-interaction-cannot-reach = You can't reach there!
\ No newline at end of file
+interaction-system-user-interaction-cannot-reach = You can't reach there!
+interaction-rate-limit-admin-announcement = Player { $player } breached interaction rate limits. They may be using macros, auto-clickers, or a modified client. Though they may just be spamming buttons or having network issues.