@@ -234,6 +234,56 @@ func (lobby *Lobby) readyToStart() bool {
234234 return hasConnectedPlayers
235235}
236236
237+ const (
238+ // Rate limiting constants
239+ // Allow up to 5 messages per second
240+ maxMessagesPerSecond = 5
241+ // Allow up to 30 messages in 20 seconds
242+ maxMessagesInWindow = 30
243+ rateLimitWindowSeconds = 20
244+ )
245+
246+ // isRateLimited checks if a player has exceeded rate limits.
247+ // Rate limits: 5 messages/second and 30 messages in 20 seconds.
248+ func isRateLimited (player * Player ) bool {
249+ now := time .Now ()
250+
251+ // Clean up old timestamps (older than 20 seconds)
252+ cutoff := now .Add (- rateLimitWindowSeconds * time .Second )
253+ validTimestamps := make ([]time.Time , 0 , len (player .messageTimestamps ))
254+ for _ , ts := range player .messageTimestamps {
255+ if ts .After (cutoff ) {
256+ validTimestamps = append (validTimestamps , ts )
257+ }
258+ }
259+ player .messageTimestamps = validTimestamps
260+
261+ // Check if exceeded 30 messages in 20 seconds window
262+ if len (player .messageTimestamps ) >= maxMessagesInWindow {
263+ return true
264+ }
265+
266+ // Check if exceeded 5 messages in the last second
267+ oneSecondAgo := now .Add (- 1 * time .Second )
268+ messagesInLastSecond := 0
269+ for _ , ts := range player .messageTimestamps {
270+ if ts .After (oneSecondAgo ) {
271+ messagesInLastSecond ++
272+ }
273+ }
274+
275+ if messagesInLastSecond >= maxMessagesPerSecond {
276+ return true
277+ }
278+
279+ return false
280+ }
281+
282+ // recordMessage adds the current timestamp to the player's message history.
283+ func recordMessage (player * Player ) {
284+ player .messageTimestamps = append (player .messageTimestamps , time .Now ())
285+ }
286+
237287func handleMessage (message string , sender * Player , lobby * Lobby ) {
238288 // Very long message can cause lags and can therefore be easily abused.
239289 // While it is debatable whether a 10000 byte (not character) long
@@ -250,18 +300,33 @@ func handleMessage(message string, sender *Player, lobby *Lobby) {
250300 return
251301 }
252302
303+ // Check if player is rate limited
304+ rateLimited := isRateLimited (sender )
305+ // Record message timestamp for rate limiting (even if rate limited)
306+ recordMessage (sender )
307+
253308 // If no word is currently selected, all players can talk to each other
254309 // and we don't have to check for corrected guesses.
255310 if lobby .CurrentWord == "" {
256- lobby .broadcastMessage (trimmedMessage , sender )
311+ if rateLimited {
312+ // Silent rate limiting: send message only to sender
313+ _ = lobby .WriteObject (sender , newMessageEvent (EventTypeMessage , trimmedMessage , sender ))
314+ } else {
315+ lobby .broadcastMessage (trimmedMessage , sender )
316+ }
257317 return
258318 }
259319
260320 if sender .State != Guessing {
261- lobby .broadcastConditional (
262- newMessageEvent (EventTypeNonGuessingPlayerMessage , trimmedMessage , sender ),
263- IsAllowedToSeeRevealedHints ,
264- )
321+ if rateLimited {
322+ // Silent rate limiting: send message only to sender
323+ _ = lobby .WriteObject (sender , newMessageEvent (EventTypeNonGuessingPlayerMessage , trimmedMessage , sender ))
324+ } else {
325+ lobby .broadcastConditional (
326+ newMessageEvent (EventTypeNonGuessingPlayerMessage , trimmedMessage , sender ),
327+ IsAllowedToSeeRevealedHints ,
328+ )
329+ }
265330 return
266331 }
267332
@@ -271,6 +336,12 @@ func handleMessage(message string, sender *Player, lobby *Lobby) {
271336 switch CheckGuess (normInput , normSearched ) {
272337 case EqualGuess :
273338 {
339+ // Don't process correct guesses if rate limited (prevents guess botting)
340+ if rateLimited {
341+ _ = lobby .WriteObject (sender , newMessageEvent (EventTypeMessage , trimmedMessage , sender ))
342+ return
343+ }
344+
274345 sender .LastScore = lobby .calculateGuesserScore ()
275346 sender .Score += sender .LastScore
276347
@@ -289,14 +360,25 @@ func handleMessage(message string, sender *Player, lobby *Lobby) {
289360 }
290361 case CloseGuess :
291362 {
292- // In cases of a close guess, we still send the message to everyone.
293- // This allows other players to guess the word by watching what the
294- // other players are misstyping.
295- lobby .broadcastMessage (trimmedMessage , sender )
296- _ = lobby .WriteObject (sender , Event {Type : EventTypeCloseGuess , Data : trimmedMessage })
363+ if rateLimited {
364+ // Silent rate limiting: send only to sender
365+ _ = lobby .WriteObject (sender , newMessageEvent (EventTypeMessage , trimmedMessage , sender ))
366+ _ = lobby .WriteObject (sender , Event {Type : EventTypeCloseGuess , Data : trimmedMessage })
367+ } else {
368+ // In cases of a close guess, we still send the message to everyone.
369+ // This allows other players to guess the word by watching what the
370+ // other players are misstyping.
371+ lobby .broadcastMessage (trimmedMessage , sender )
372+ _ = lobby .WriteObject (sender , Event {Type : EventTypeCloseGuess , Data : trimmedMessage })
373+ }
297374 }
298375 default :
299- lobby .broadcastMessage (trimmedMessage , sender )
376+ if rateLimited {
377+ // Silent rate limiting: send message only to sender
378+ _ = lobby .WriteObject (sender , newMessageEvent (EventTypeMessage , trimmedMessage , sender ))
379+ } else {
380+ lobby .broadcastMessage (trimmedMessage , sender )
381+ }
300382 }
301383}
302384
0 commit comments