This patch implements chat rate limiting.
The controlling cvar is g_chatRateLimit, which defaults to 0. (I'd suggest defaulting it to 1, for obvious reasons, but I wanted to play conservatively.)
There are two other cvars:
* g_chatMaxPoints: The maximum number of 'points' you can accrue from chatting before you get rate limited. This defaults to 3000.
* g_chatSafeTime: The minimum amount of time (in milliseconds) you have to wait between chats to not get 'points'. This defaults to 1500.
The way it works is simple. Each time you chat, the time is recorded. If you chat again, and it's quicker than g_chatSafeTime millis since your last chat, you accrue the difference (so the longer you wait, the less points you get). If you wait /longer/ than g_chatSafeTime millis, you lose the number of millis past g_chatSafeTime from your points (which is how you can chat again).
I played around with the values. The current one lets you tap an 'Incoming!' macro three times in rapid succession before getting rate limited, which is, I think, a reasonable limit on chat wastage.
As an added note, this patch throws away blank messages, keeping them from chat. (They shouldn't get there in a pure client, but Just In Case(tm).) That two-line chunk is from R1CH's mods.
I would rather see a more generic implementation that can be used for more than just chat. SOON we will have voice-chats and perhaps private messaging. Not to mention other spammable things need to be flood protected like rconAuth, ready, team, and possibly others.
I like the functionality forty added to etpub for this: g_floodThreshold (default 6) means they are allowed a floating window of 6 commands. The maximum protected command rate is hardcoded at 1 per second, but you can say 6 things really quick, but then you have to wait 7 seconds before you can issue another protected command.
This is trivial to make "more generic," just by changing the variable names (lastLimitedTime instead of lastChatTime) and hooking into the functionality in different places.
My patch in bug 2812 fixes part of the teamspam problem; the other can be tied into this.
I'll rework the 'check' into a function that can be called and will return qtrue or qfalse depending on whether they should be allowed to run a protected command, and modify chat to use the generic implementation. Others can then add protection to other commands.
Also, by setting the safeTime to 6000 and the chatMaxPoints to 36000 you can duplicate the floodThreshold implementation. In general, for any g_floodThreshold value n, setting safeTime to 1000*n and maxPoints to 1000*n*n you get a duplicate result.
Created attachment 1002[details]
New generic rate limiting system.
This patch makes the rate limiting generic. The cvars have been renamed:
* g_rateLimit is the controlling cvar;
* g_rateLimitMaxPoints controls the maximum number of points a player can have before being rate limited; and
* g_rateLimitSafeTime is the number of milliseconds that it is 'safe' to chat afterwards.
A new function, G_Rate_Limited, handles the rate limiting.
This patch still just does rate limiting for chat, but it is now trivial to hook into the new function G_Rate_Limited( ent ) for any type of message that should be rate-limited.
After discussion with tjw, I've written a slightly-modified version of the previous patch which uses two cvars instead of three. It also changes the names to things relating to 'flooding,' which may be a better name.
Created attachment 1010[details]
New g_floodMinTime/g_floodMaxDemerits patch
Note that this does NOT obsolete the previous patch; it is a somewhat alternative implementation. I think it behaves somewhat less intuitively, but tjw prefers less cvars.
There are now only two cvars:
* g_floodMinTime
* g_floodMaxDemerits
If g_floodMinTime is 0, then flood limiting is off. If it's another value, it's the minimum number of milliseconds before it is considered 'floody'. (This is equivalent to g_rateLimitSafeTime in the previous patch.)
If g_floodMaxDemerits is 0, and flood limiting is on, then it assumes that the server operators wants to operate in an "easiest to maintain" mode. It effectively makes it so that you can send N messages in N seconds without it being a flood, where N is g_floodMinTime / 1000 (since it's in milliseconds). If you set g_floodMaxDemerits to something other than 0, it behaves identically to g_rateLimitMaxPoints from the previous implementation.
Created attachment 1447[details]
Above patch modified to respect admin nocensorflood flag.
Above patch modified to respect admin nocensorflood flag. Perhaps this minor two-line fix will make it commitable.
Created attachment 1452[details]
Added protection to admin !commands
Commands like !time and !admintest were still spammable, so I added a check in g_admin_command_check to apply this to admin commands.
Just a point: Lakitu's gently-modified version of this patch is in pretty heavy use out in the Tremulous wild. (In case you're wondering if it needs more testing before being added to SVN.)
Created attachment 1641[details]
Adaptation of Phil's code
This is just an adaptation of Phil Bordelon's code. It should function pretty much the same, but this makes g_floodMaxDemerits the controlling (i.e., 0 = don't bother) cvar instead of g_floodMinTime (which shouldn't make a big difference).
Some code in Phil's patch forgot that x + y = x - -y. The replacement code should* be functionally identical.
Variables were renamed, hopefully remaining just as clear: lastFloodTime to messageTime; floodDemerits to demerits. The second underscore was also removed from G_Flood_Limited().
There is now only a single call to G_FloodLimited(): in ClientCommand() for commands with the CMD_MESSAGE flag. This obviates the need for separate calls everywhere they would be useful.
As a disclaimer, all the above "should"s are subject to my ability to think straight at the moment, and I haven't actually tested the code to see if it works right.
Created attachment 1642[details]
New implementation
I think the above implementation can be rather annoying, so I tried making a more "friendly" implementation. It approached the problem a bit differently, is a bit more complex, and has an extra cvar.
Like Phil's implementation (or my adaptation of it), this keeps track of demerits. Above a certain number of demerits, messages won't be sent. Above a certain (possibly different) number of demerits, a user will be warned that they are flooding. Above a certain (again, possibly different) number of demerits, a user will be muted.
g_floodThreshold: the demerit threshold above which no messages will be sent (equivalent to g_floodMaxDemerits).
g_floodWarn: a value above which a user will be warned that they are flooding.
g_floodMute: a value above which a user will be muted.
This implementation takes into account the size of messages being sent and the amount of seconds elapsed since the last message was sent, rewarding longer waits between messages.
With values like g_floodThreshold = 175 and g_floodMute = 500, spamming results in something like: three long messages can be sent in quick succession, with the fourth long message being blocked. It should even allow a person to rapidly send small messages, as long as they do not continue for too long. g_floodWarn allows warnings to be sent before (or after or as) messages get blocked.
Like all automated systems, this can't detect if a person is being annoying. So this implementation tends to be more forgiving, but it should also work pretty well at preventing people from "flooding" others off a server.
(In reply to comment #16)
Making the code cleaner via CMD_MESSAGE is a great idea, but auto-!mute isn't the way to go with this. If !mute had a timer where they could be set to unmute after some time, that would be fine, but that's note the case. This is a team game. If someone is !muted, they can't play it as a team. Handing out a !mute that lasts until nextmap for spamming a bind a couple of times is hardly a punishment that fits the crime. Take Phil's/my old system and fit it into the CMD_MESSAGE framework instead.
One other note: 1641, 1642, and 1829 do not address flooding via admin commands that echo to everyone, such as !admintest or !time. It will work when they are used via /say !command, but not when used directly from clientcommand via /!command. I will try to get around to fixing that sometime.
(In reply to comment #17)
> auto-!mute isn't
> the way to go with this. If !mute had a timer where they could be set to unmute
> after some time, that would be fine, but that's note the case. This is a team
> game. If someone is !muted, they can't play it as a team.
>
Teammates can call unmute team votes (since r925). Also, as per comment #16, thresholds can be modified or functionality disabled independently (everything is disabled by default).
(In reply to comment #20)
> One other note: 1641, 1642, and 1829 do not address flooding via admin commands
> that echo to everyone, such as !admintest or !time.
>
s/!time// (since r834)
Created attachment 999 [details] Patch to implement g_chatRateLimit This patch implements chat rate limiting as described above. Tested on Arcadia.
Created attachment 1002 [details] New generic rate limiting system. This patch makes the rate limiting generic. The cvars have been renamed: * g_rateLimit is the controlling cvar; * g_rateLimitMaxPoints controls the maximum number of points a player can have before being rate limited; and * g_rateLimitSafeTime is the number of milliseconds that it is 'safe' to chat afterwards. A new function, G_Rate_Limited, handles the rate limiting. This patch still just does rate limiting for chat, but it is now trivial to hook into the new function G_Rate_Limited( ent ) for any type of message that should be rate-limited.
Created attachment 1010 [details] New g_floodMinTime/g_floodMaxDemerits patch Note that this does NOT obsolete the previous patch; it is a somewhat alternative implementation. I think it behaves somewhat less intuitively, but tjw prefers less cvars. There are now only two cvars: * g_floodMinTime * g_floodMaxDemerits If g_floodMinTime is 0, then flood limiting is off. If it's another value, it's the minimum number of milliseconds before it is considered 'floody'. (This is equivalent to g_rateLimitSafeTime in the previous patch.) If g_floodMaxDemerits is 0, and flood limiting is on, then it assumes that the server operators wants to operate in an "easiest to maintain" mode. It effectively makes it so that you can send N messages in N seconds without it being a flood, where N is g_floodMinTime / 1000 (since it's in milliseconds). If you set g_floodMaxDemerits to something other than 0, it behaves identically to g_rateLimitMaxPoints from the previous implementation.
Created attachment 1447 [details] Above patch modified to respect admin nocensorflood flag. Above patch modified to respect admin nocensorflood flag. Perhaps this minor two-line fix will make it commitable.
Created attachment 1452 [details] Added protection to admin !commands Commands like !time and !admintest were still spammable, so I added a check in g_admin_command_check to apply this to admin commands.
Created attachment 1641 [details] Adaptation of Phil's code This is just an adaptation of Phil Bordelon's code. It should function pretty much the same, but this makes g_floodMaxDemerits the controlling (i.e., 0 = don't bother) cvar instead of g_floodMinTime (which shouldn't make a big difference). Some code in Phil's patch forgot that x + y = x - -y. The replacement code should* be functionally identical. Variables were renamed, hopefully remaining just as clear: lastFloodTime to messageTime; floodDemerits to demerits. The second underscore was also removed from G_Flood_Limited(). There is now only a single call to G_FloodLimited(): in ClientCommand() for commands with the CMD_MESSAGE flag. This obviates the need for separate calls everywhere they would be useful. As a disclaimer, all the above "should"s are subject to my ability to think straight at the moment, and I haven't actually tested the code to see if it works right.
Created attachment 1642 [details] New implementation I think the above implementation can be rather annoying, so I tried making a more "friendly" implementation. It approached the problem a bit differently, is a bit more complex, and has an extra cvar. Like Phil's implementation (or my adaptation of it), this keeps track of demerits. Above a certain number of demerits, messages won't be sent. Above a certain (possibly different) number of demerits, a user will be warned that they are flooding. Above a certain (again, possibly different) number of demerits, a user will be muted. g_floodThreshold: the demerit threshold above which no messages will be sent (equivalent to g_floodMaxDemerits). g_floodWarn: a value above which a user will be warned that they are flooding. g_floodMute: a value above which a user will be muted. This implementation takes into account the size of messages being sent and the amount of seconds elapsed since the last message was sent, rewarding longer waits between messages. With values like g_floodThreshold = 175 and g_floodMute = 500, spamming results in something like: three long messages can be sent in quick succession, with the fourth long message being blocked. It should even allow a person to rapidly send small messages, as long as they do not continue for too long. g_floodWarn allows warnings to be sent before (or after or as) messages get blocked. Like all automated systems, this can't detect if a person is being annoying. So this implementation tends to be more forgiving, but it should also work pretty well at preventing people from "flooding" others off a server.
Created attachment 1828 [details] Mine/Phil's Against 1102